Coverage for src/lilbee/server/handlers/analyze.py: 100%

34 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-11-06 08:17 +0000

1"""Analyze handlers: the SSE-streamed run, and the analyze tip state.""" 

2 

3from __future__ import annotations 

4 

5import asyncio 

6from collections.abc import AsyncGenerator 

7 

8from litestar.exceptions import HTTPException 

9from litestar.status_codes import HTTP_503_SERVICE_UNAVAILABLE 

10 

11from lilbee.app.analyze import AnalyzeReport, AnalyzeRequest, hide_tip, run_analysis, tip_state 

12from lilbee.app.profiles import file_failure_message 

13from lilbee.app.services import get_services 

14from lilbee.core.config import cfg 

15from lilbee.server.handlers.sse import SseStream 

16from lilbee.server.models import AnalyzeResponse, AnalyzeStateResponse 

17 

18 

19async def _run(sse: SseStream, request: AnalyzeRequest) -> AnalyzeReport: 

20 """Run analyze with the stream's progress and cancel; the drain ends when this task does.""" 

21 try: 

22 return await run_analysis( 

23 get_services().profile_store, request, on_progress=sse.callback, cancel=sse.cancel 

24 ) 

25 except OSError as exc: 

26 raise RuntimeError(file_failure_message(exc)) from exc 

27 

28 

29def _done_payload(report: AnalyzeReport) -> dict[str, object]: 

30 return AnalyzeResponse.from_report(report).model_dump(mode="json") 

31 

32 

33async def analyze_stream(request: AnalyzeRequest) -> AsyncGenerator[str, None]: 

34 """Yield ``analyze`` progress events, then ``done`` with the report or ``error``.""" 

35 sse = SseStream() 

36 task = asyncio.create_task(_run(sse, request)) 

37 async for event in sse.drain(task, "Analyze stream"): 

38 yield event 

39 frame = sse.terminal_frame(task, _done_payload) 

40 if frame is not None: 

41 yield frame 

42 

43 

44async def analyze_state() -> AnalyzeStateResponse: 

45 """The project's analyze tip state.""" 

46 return AnalyzeStateResponse.from_state(await asyncio.to_thread(tip_state, cfg.data_root)) 

47 

48 

49async def dismiss_tip() -> AnalyzeStateResponse: 

50 """Hide the analyze tip for the project and return the new state; a failed write is 503.""" 

51 try: 

52 await asyncio.to_thread(hide_tip, cfg.data_root) 

53 except OSError as exc: 

54 raise HTTPException( 

55 status_code=HTTP_503_SERVICE_UNAVAILABLE, detail=file_failure_message(exc) 

56 ) from exc 

57 return await analyze_state()