Coverage for src/lilbee/crawler/models.py: 100%
40 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-04 17:08 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-04 17:08 +0000
1"""Backend-agnostic value types crossing the runner/fetcher seam."""
3from __future__ import annotations
5import threading
6from dataclasses import dataclass, field
7from typing import TypeAlias
9# Explicit "no page limit" for a crawl. Distinct from None, which means
10# "unspecified, use the protective default cfg.crawl_safety_max_pages".
11CRAWL_PAGES_UNLIMITED = 0
14@dataclass
15class CrawlResult:
16 """Outcome of crawling a single URL.
18 This is the high-level result surfaced to lilbee callers
19 (CLI, MCP, HTTP, TUI). The adapter produces ``FetchedPage``
20 and the orchestration layer converts it to ``CrawlResult``
21 when returning up to the caller.
22 """
24 url: str
25 markdown: str = ""
26 success: bool = True
27 error: str | None = None
29 def failure_reason(self) -> str | None:
30 """Why this page has nothing to save, or None when it does.
32 The save layer skips exactly these pages, so the event layer reports
33 failures from the same predicate: the two cannot drift apart.
34 """
35 if not self.success:
36 return self.error or "fetch failed"
37 if not self.markdown.strip():
38 return "page produced empty markdown"
39 return None
42@dataclass
43class FetchedPage:
44 """Single page produced by a ``WebFetcher`` backend.
46 Distinct from :class:`CrawlResult` so the adapter surface
47 stays narrow and neutral: just the bytes we needed out of
48 the underlying SDK's response object.
49 """
51 url: str
52 markdown: str = ""
53 success: bool = True
54 error: str | None = None
55 links: list[str] = field(default_factory=list)
58@dataclass
59class ConcurrencySpec:
60 """Backend-agnostic concurrency + rate-limit knobs.
62 The crawl4ai adapter translates these into ``RateLimiter`` and
63 ``SemaphoreDispatcher`` calls; a future adapter with its own
64 BFS loop maps them onto ``asyncio.Semaphore`` + retry logic.
65 """
67 semaphore_count: int = 1
68 mean_delay: float = 0.0
69 max_delay_range: float = 0.0
70 retry_on_rate_limit: bool = False
71 retry_base_delay_min: float = 0.0
72 retry_base_delay_max: float = 0.0
73 retry_max_backoff: float = 0.0
74 retry_max_attempts: int = 0
77@dataclass
78class FilterSpec:
79 """Backend-agnostic filter settings applied to discovered links.
81 Pure Python data; each adapter decides how to plug the settings
82 into its own filter pipeline.
83 """
85 exclude_patterns: list[str] = field(default_factory=list)
86 include_subdomains: bool = False
89CancelToken: TypeAlias = threading.Event
90"""Cancellation handle the orchestration layer passes to a fetcher.
92An already-``set()`` event means "stop as soon as you can". The
93crawl4ai adapter polls it in both its streaming loop and its BFS
94strategy's ``should_cancel`` hook; a future adapter can poll it
95in whatever granularity it supports.
96"""