Coverage for src/lexigram/web/integrations/setup.py: 27%
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"""ASGI lifespan and application setup helpers for the web provider."""
3from __future__ import annotations
5from collections.abc import AsyncIterator
6from contextlib import asynccontextmanager
7import inspect
8from typing import TYPE_CHECKING
10from lexigram.logging import get_logger
11from lexigram.web.hooks import WebServerStartedHook, WebServerStoppedHook
13if TYPE_CHECKING:
14 from starlette.applications import Starlette
16logger = get_logger(__name__)
19async def _emit_action(app: Starlette, hook_name: str, payload: object) -> None:
20 """Emit a lifecycle action hook when the app is wired with a registry."""
21 hooks = getattr(app.state, "hook_registry", None)
22 if hooks is None:
23 return
25 await hooks.call_action(hook_name, payload=payload)
28@asynccontextmanager
29async def lifespan(app: Starlette) -> AsyncIterator[None]:
30 """ASGI lifespan manager for the Starlette application.
32 Handles startup diagnostics and graceful shutdown of shared resources
33 attached to ``app.state`` (database pool, Redis client, etc.).
34 """
35 logger.info("Starting web application lifespan")
37 # Initialize background_tasks if not already present
38 if not hasattr(app.state, "background_tasks"):
39 app.state.background_tasks = []
41 # Diagnostics for tests that expect debug logs on missing resources
42 if not hasattr(app.state, "db_pool"):
43 logger.debug(
44 "Database pool not found in app state, skipping cleanup registration",
45 )
47 await _emit_action(app, "server.started", WebServerStartedHook())
49 yield
51 logger.info("Shutting down web application lifespan")
53 # 1. Cleanup DB Pool
54 if hasattr(app.state, "db_pool"):
55 try:
56 pool = app.state.db_pool
57 if hasattr(pool, "close"):
58 res = pool.close()
59 if hasattr(res, "__await__") or inspect.isawaitable(res):
60 await res
61 except Exception: # noqa: BLE001 — best-effort DB pool cleanup, must not crash shutdown
62 logger.exception("Error closing DB pool")
64 # 2. Cleanup Redis Client
65 if hasattr(app.state, "redis_client"):
66 try:
67 client = app.state.redis_client
68 if hasattr(client, "close"):
69 res = client.close()
70 if hasattr(res, "__await__") or inspect.isawaitable(res):
71 await res
72 except Exception: # noqa: BLE001 — best-effort Redis client cleanup, must not crash shutdown
73 logger.exception("Error closing Redis client")
75 await _emit_action(app, "server.stopped", WebServerStoppedHook())
77 logger.debug("Web application lifespan ended")
80__all__ = ["lifespan"]