Coverage for src/lilbee/server/handlers/crawl.py: 100%
20 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
1"""Crawl streaming handler."""
3from __future__ import annotations
5import asyncio
6from collections.abc import AsyncGenerator
7from pathlib import Path
9from lilbee.core.config.enums import CrawlRenderMode
10from lilbee.crawler.models import NO_LOGIN, CrawlLogin
11from lilbee.server.handlers.sse import SseStream
14async def crawl_stream(
15 url: str,
16 depth: int | None = None,
17 max_pages: int | None = None,
18 render_mode: CrawlRenderMode | None = None,
19 include_subdomains: bool = False,
20 login: CrawlLogin = NO_LOGIN,
21) -> AsyncGenerator[str, None]:
22 """Stream crawl progress as SSE events.
24 Emits crawl_start, crawl_page, crawl_done events, then a final done event
25 with the list of files written. On error emits crawl_error.
26 Sets a cancel event on client disconnect so the crawl stops between pages.
28 A browser crawl that finds no Chromium installed inlines
29 setup_start/progress/done events before the crawl begins, so a consumer can
30 render a matching 'setup' progress indicator. These are not part of every
31 stream: an http crawl never launches a browser, and that is the default
32 render mode, so a client must not block waiting for a setup phase.
33 """
34 sse = SseStream()
36 async def _run_crawl() -> list[Path]:
37 from lilbee.crawler import crawl_and_save
39 # crawl_and_save runs the Chromium bootstrap itself on first use,
40 # relaying setup_* events through the same on_progress callback
41 # so the SSE stream carries them before any crawl_* events.
42 try:
43 return await crawl_and_save(
44 url,
45 depth=depth,
46 max_pages=max_pages,
47 on_progress=sse.callback,
48 cancel=sse.cancel,
49 include_subdomains=include_subdomains,
50 render_mode=render_mode,
51 login=login,
52 )
53 finally:
54 sse.queue.put_nowait(None)
56 task = asyncio.create_task(_run_crawl())
57 async for event in sse.drain(task, "Crawl stream"):
58 yield event
59 frame = sse.terminal_frame(task, lambda paths: {"files_written": [str(p) for p in paths]})
60 if frame is not None:
61 yield frame