Coverage for src/lexigram/web/interceptors/pipeline.py: 45%
44 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"""Interceptor pipeline for chaining interceptors.
3The pipeline executes interceptors in order, passing control to the next
4interceptor until the handler is reached.
5"""
7from __future__ import annotations
9from typing import TYPE_CHECKING, Any
11from lexigram.web.protocols import (
12 CallHandlerProtocol,
13 ExecutionContextProtocol,
14 WebInterceptorProtocol,
15)
17if TYPE_CHECKING:
18 from collections.abc import Callable
21class DefaultCallHandler(CallHandlerProtocol):
22 """Default call handler that executes the actual handler function."""
24 def __init__(self, handler: Callable):
25 """Initialize with the handler function.
27 Args:
28 handler: The actual handler function to call.
29 """
30 self._handler = handler
32 async def handle(self) -> Any:
33 """Execute the handler and return its result."""
34 return await self._handler()
37class InterceptorChain(CallHandlerProtocol):
38 """Internal chain that links interceptors together."""
40 def __init__(
41 self,
42 interceptors: list[WebInterceptorProtocol],
43 index: int,
44 final_handler: Callable,
45 context: ExecutionContextProtocol,
46 ):
47 """Initialize the chain.
49 Args:
50 interceptors: List of interceptors to chain.
51 index: Current index in the interceptor list.
52 final_handler: The handler to call when all interceptors complete.
53 context: The execution context.
54 """
55 self._interceptors = interceptors
56 self._index = index
57 self._final_handler = final_handler
58 self._context = context
60 async def handle(self) -> Any:
61 """Execute the next interceptor or final handler."""
62 if self._index >= len(self._interceptors):
63 # All interceptors passed, execute final handler
64 return await self._final_handler()
66 # Get current interceptor and create next in chain
67 interceptor = self._interceptors[self._index]
68 next_chain = InterceptorChain(
69 self._interceptors,
70 self._index + 1,
71 self._final_handler,
72 self._context,
73 )
75 # Call the interceptor
76 return await interceptor.intercept(self._context, next_chain)
79class InterceptorPipeline:
80 """Pipeline that manages and executes interceptors.
82 Interceptors are executed in order, each having the opportunity to:
83 - Execute code before the handler
84 - Call the next interceptor/handler
85 - Execute code after the handler (by transforming the result)
86 - Short-circuit by returning early without calling next
87 """
89 def __init__(self, interceptors: list[WebInterceptorProtocol] | None = None):
90 """Initialize the interceptor pipeline.
92 Args:
93 interceptors: Optional list of interceptors to register.
94 """
95 self._interceptors: list[WebInterceptorProtocol] = (
96 list(interceptors) if interceptors else []
97 )
99 def add_interceptor(self, interceptor: WebInterceptorProtocol) -> None:
100 """Add an interceptor to the pipeline.
102 Interceptors are executed in the order they are added.
104 Args:
105 interceptor: The interceptor to add.
106 """
107 if interceptor not in self._interceptors:
108 self._interceptors.append(interceptor)
110 def remove_interceptor(self, interceptor: WebInterceptorProtocol) -> None:
111 """Remove an interceptor from the pipeline.
113 Args:
114 interceptor: The interceptor to remove.
115 """
116 if interceptor in self._interceptors:
117 self._interceptors.remove(interceptor)
119 def clear(self) -> None:
120 """Remove all interceptors from the pipeline."""
121 self._interceptors.clear()
123 @property
124 def interceptors(self) -> list[WebInterceptorProtocol]:
125 """Get the list of registered interceptors."""
126 return list(self._interceptors)
128 async def execute(
129 self,
130 context: ExecutionContextProtocol,
131 handler: Callable,
132 ) -> Any:
133 """Execute the interceptor pipeline.
135 Args:
136 context: The execution context.
137 handler: The final handler to execute.
139 Returns:
140 The result of the handler (possibly transformed by interceptors).
141 """
142 if not self._interceptors:
143 # No interceptors, execute handler directly
144 return await handler()
146 # Create the chain starting with first interceptor
147 chain = InterceptorChain(
148 self._interceptors,
149 0,
150 handler,
151 context,
152 )
154 return await chain.handle()
156 def __len__(self) -> int:
157 """Return the number of interceptors."""
158 return len(self._interceptors)
160 def __bool__(self) -> bool:
161 """Return True if there are interceptors."""
162 return bool(self._interceptors)
165__all__ = ["DefaultCallHandler", "InterceptorChain", "InterceptorPipeline"]