Coverage for src / lexigram / contracts / web / http_protocols.py: 100%

51 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-19 05:41 +0800

1"""Service mesh protocols. 

2 

3Protocols for service discovery, load balancing, and communication. 

4""" 

5 

6from __future__ import annotations 

7 

8from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable 

9 

10from lexigram.contracts.web.http_types import ServiceInfo as ServiceInfo 

11 

12if TYPE_CHECKING: 

13 from lexigram.contracts.web.http_models import HttpResponse 

14 

15 

16@runtime_checkable 

17class ServiceMeshRegistryProtocol(Protocol): 

18 """Protocol for service registry implementations. 

19 

20 The service registry manages service instance registration 

21 and discovery. 

22 

23 Example: 

24 ```python 

25 class ConsulRegistry: 

26 async def register(self, service: ServiceInfo) -> None: 

27 await self._client.agent.service.register( 

28 name=service.name, 

29 address=service.host, 

30 port=service.port, 

31 ) 

32 ``` 

33 """ 

34 

35 async def register(self, service: ServiceInfo) -> None: 

36 """Register a service instance. 

37 

38 Args: 

39 service: ServiceInfo to register. 

40 """ 

41 ... 

42 

43 async def deregister(self, service_name: str, host: str, port: int) -> None: 

44 """Deregister a service instance. 

45 

46 Args: 

47 service_name: Name of the service. 

48 host: Service host. 

49 port: Service port. 

50 """ 

51 ... 

52 

53 async def discover(self, service_name: str) -> list[ServiceInfo]: 

54 """Discover all instances of a service. 

55 

56 Args: 

57 service_name: Name of the service. 

58 

59 Returns: 

60 List of ServiceInfo instances. 

61 """ 

62 ... 

63 

64 async def get_service( 

65 self, 

66 service_name: str, 

67 host: str, 

68 port: int, 

69 ) -> ServiceInfo | None: 

70 """Get a specific service instance. 

71 

72 Args: 

73 service_name: Name of the service. 

74 host: Service host. 

75 port: Service port. 

76 

77 Returns: 

78 ServiceInfo if found, None otherwise. 

79 """ 

80 ... 

81 

82 async def list_services(self) -> list[str]: 

83 """List all registered service names. 

84 

85 Returns: 

86 List of service names. 

87 """ 

88 ... 

89 

90 

91@runtime_checkable 

92class SelectorProtocol(Protocol): 

93 """Protocol for load balancing selectors. 

94 

95 Selectors choose which service instance to route to. 

96 """ 

97 

98 async def select(self, instances: list[ServiceInfo]) -> ServiceInfo | None: 

99 """Select an instance from the available instances. 

100 

101 Args: 

102 instances: List of available service instances. 

103 

104 Returns: 

105 Selected instance or None. 

106 """ 

107 ... 

108 

109 

110@runtime_checkable 

111class HTTPSessionProtocol(Protocol): 

112 """Protocol for underlying HTTP session implementations. 

113 

114 This allows the HTTPClient to be backend-agnostic, enabling easier 

115 testing and support for different HTTP libraries. 

116 """ 

117 

118 async def request(self, method: str, url: str, **kwargs: Any) -> Any: 

119 """Perform an HTTP request and return a raw response object.""" 

120 ... 

121 

122 async def close(self) -> None: 

123 """Close the session and release resources.""" 

124 ... 

125 

126 

127@runtime_checkable 

128class HTTPClientProtocol(Protocol): 

129 """Protocol for HTTP client implementations.""" 

130 

131 async def start(self) -> None: 

132 """Start the HTTP client and its underlying connection pool.""" 

133 ... 

134 

135 async def stop(self) -> None: 

136 """Stop the HTTP client and release all connection resources.""" 

137 ... 

138 

139 async def request(self, method: str, url: str, **kwargs: Any) -> HttpResponse: 

140 """Perform an arbitrary HTTP request. 

141 

142 Args: 

143 method: HTTP method (GET, POST, PUT, …). 

144 url: Request URL. 

145 **kwargs: Additional options (headers, data, json, params, …). 

146 

147 Returns: 

148 Framework-owned :class:`HttpResponse`. 

149 """ 

150 ... 

151 

152 async def get(self, url: str, **kwargs: Any) -> HttpResponse: 

153 """Perform GET request. 

154 

155 Args: 

156 url: Request URL. 

157 **kwargs: Additional options. 

158 

159 Returns: 

160 Framework-owned :class:`HttpResponse`. 

161 """ 

162 ... 

163 

164 async def post(self, url: str, **kwargs: Any) -> HttpResponse: 

165 """Perform POST request. 

166 

167 Args: 

168 url: Request URL. 

169 **kwargs: Additional options (e.g. ``json=``, ``data=``). 

170 

171 Returns: 

172 Framework-owned :class:`HttpResponse`. 

173 """ 

174 ... 

175 

176 async def put(self, url: str, **kwargs: Any) -> HttpResponse: 

177 """Perform PUT request. 

178 

179 Args: 

180 url: Request URL. 

181 **kwargs: Additional options. 

182 

183 Returns: 

184 Framework-owned :class:`HttpResponse`. 

185 """ 

186 ... 

187 

188 async def delete(self, url: str, **kwargs: Any) -> HttpResponse: 

189 """Perform DELETE request. 

190 

191 Args: 

192 url: Request URL. 

193 **kwargs: Additional options. 

194 

195 Returns: 

196 Framework-owned :class:`HttpResponse`. 

197 """ 

198 ... 

199 

200 async def patch(self, url: str, **kwargs: Any) -> HttpResponse: 

201 """Perform PATCH request. 

202 

203 Args: 

204 url: Request URL. 

205 **kwargs: Additional options. 

206 

207 Returns: 

208 Framework-owned :class:`HttpResponse`. 

209 """ 

210 ... 

211 

212 async def head(self, url: str, **kwargs: Any) -> HttpResponse: 

213 """Perform HEAD request. 

214 

215 Args: 

216 url: Request URL. 

217 **kwargs: Additional options. 

218 

219 Returns: 

220 Framework-owned :class:`HttpResponse`. 

221 """ 

222 ... 

223 

224 

225@runtime_checkable 

226class InterceptorProtocol(Protocol): 

227 """Protocol for request/response interceptors. 

228 

229 Interceptors are applied to every request/response cycle in 

230 :class:`~lexigram.http.HTTPClient`. Implementations receive typed 

231 ``RequestContext`` objects and may modify them before the request is 

232 dispatched, or inspect / annotate the raw response after it arrives. 

233 

234 Example: 

235 class LoggingInterceptor: 

236 async def intercept_request(self, context: Any) -> Any: 

237 logger.info("outbound_request", method=context.method, url=context.url) 

238 return context 

239 

240 async def intercept_response(self, response: Any) -> Any: 

241 logger.info("inbound_response", status=response.status) 

242 return response 

243 """ 

244 

245 async def intercept_request(self, context: Any) -> Any: 

246 """Called before a request is dispatched. 

247 

248 Args: 

249 context: :class:`~lexigram.http.RequestContext` for the outbound 

250 request. Implementations may mutate and return it. 

251 

252 Returns: 

253 The (possibly modified) request context. 

254 """ 

255 ... 

256 

257 async def intercept_response(self, response: Any) -> Any: 

258 """Called after a response is received from the server. 

259 

260 Args: 

261 response: Raw response object from the underlying HTTP library. 

262 Implementations may annotate or replace it. 

263 

264 Returns: 

265 The (possibly modified) response. 

266 """ 

267 ... 

268 

269 

270@runtime_checkable 

271class InterceptorChainProtocol(Protocol): 

272 """Protocol for interceptor chain management. 

273 

274 Manages a collection of interceptors and orchestrates their execution 

275 in sequence. Each interceptor in the chain processes the request/response 

276 before passing control to the next interceptor. 

277 

278 Example: 

279 ```python 

280 class InterceptorChain: 

281 def __init__(self, interceptors: list[InterceptorProtocol]): 

282 self._interceptors = interceptors 

283 

284 async def execute_request(self, context: Any) -> Any: 

285 for interceptor in self._interceptors: 

286 context = await interceptor.intercept_request(context) 

287 return context 

288 

289 async def execute_response(self, response: Any) -> Any: 

290 for interceptor in reversed(self._interceptors): 

291 response = await interceptor.intercept_response(response) 

292 return response 

293 ``` 

294 """ 

295 

296 def add_interceptor(self, interceptor: InterceptorProtocol) -> None: 

297 """Add an interceptor to the chain. 

298 

299 Args: 

300 interceptor: The interceptor to add. 

301 """ 

302 ... 

303 

304 def remove_interceptor(self, interceptor: InterceptorProtocol) -> None: 

305 """Remove an interceptor from the chain. 

306 

307 Args: 

308 interceptor: The interceptor to remove. 

309 """ 

310 ... 

311 

312 async def process_request(self, context: Any) -> Any: 

313 """Process request through the interceptor chain. 

314 

315 Args: 

316 context: Request context to process. 

317 

318 Returns: 

319 Modified request context after all interceptors. 

320 """ 

321 ... 

322 

323 async def process_response(self, response: Any) -> Any: 

324 """Process response through the interceptor chain. 

325 

326 Args: 

327 response: Response to process. 

328 

329 Returns: 

330 Modified response after all interceptors. 

331 """ 

332 ... 

333 

334 

335@runtime_checkable 

336class ConnectMetricsCollectorProtocol(Protocol): 

337 """Protocol for service metrics collection.""" 

338 

339 def record_request( 

340 self, 

341 service: str, 

342 method: str, 

343 duration: float, 

344 status: int, 

345 ) -> None: 

346 """Record a request metric.""" 

347 ... 

348 

349 def get_metrics(self, service: str) -> dict[str, Any]: 

350 """Get metrics for a service.""" 

351 ... 

352 

353 

354@runtime_checkable 

355class WebSocketProtocol(Protocol): 

356 """Framework-agnostic WebSocket connection contract. 

357 

358 Abstracts over concrete WebSocket objects (Starlette, AIOHTTP, …) so 

359 the GraphQL subscription transport layer stays decoupled from the 

360 underlying web framework. 

361 """ 

362 

363 async def accept( 

364 self, 

365 subprotocol: str | None = None, 

366 ) -> None: 

367 """Accept the WebSocket upgrade handshake. 

368 

369 Args: 

370 subprotocol: Optional WebSocket sub-protocol to negotiate 

371 (e.g. ``"graphql-transport-ws"``). 

372 """ 

373 ... 

374 

375 async def receive_text(self) -> str: 

376 """Receive the next text frame from the client. 

377 

378 Returns: 

379 Raw text payload of the received frame. 

380 """ 

381 ... 

382 

383 async def receive_json(self) -> Any: 

384 """Receive the next frame and deserialise it as JSON. 

385 

386 Returns: 

387 Deserialised JSON value. 

388 """ 

389 ... 

390 

391 async def send_text(self, data: str) -> None: 

392 """Send a text frame to the client. 

393 

394 Args: 

395 data: Text payload to send. 

396 """ 

397 ... 

398 

399 async def send_json(self, data: Any) -> None: 

400 """Serialise *data* as JSON and send it to the client. 

401 

402 Args: 

403 data: Value to serialise and send. 

404 """ 

405 ... 

406 

407 async def close(self, code: int = 1000, reason: str = "") -> None: 

408 """Close the WebSocket connection. 

409 

410 Args: 

411 code: WebSocket close status code (default 1000 = normal closure). 

412 reason: Human-readable close reason. 

413 """ 

414 ... 

415 

416 

417__all__ = [ 

418 "ConnectMetricsCollectorProtocol", 

419 "HTTPClientProtocol", 

420 "HTTPSessionProtocol", 

421 "InterceptorChainProtocol", 

422 "InterceptorProtocol", 

423 "SelectorProtocol", 

424 "ServiceInfo", 

425 "ServiceMeshRegistryProtocol", 

426 "WebSocketProtocol", 

427]