"""Manifest I/O for reference discovery and crawl results.""" from __future__ import annotations import json from datetime import datetime from pathlib import Path from refcurator.models import CrawlResult, CrawlResultEntry, Manifest, ManifestURL from refcurator.utils import normalize_url def read_manifest(path: Path) -> Manifest: """Read a manifest JSON file.""" data = json.loads(path.read_text()) return Manifest(**data) def write_manifest(manifest: Manifest, path: Path) -> None: """Write a manifest to a JSON file.""" path.parent.mkdir(parents=True, exist_ok=True) path.write_text(manifest.model_dump_json(indent=2)) def merge_manifests(manifests: list[Manifest]) -> Manifest: """Merge multiple manifests, deduplicating URLs.""" seen: dict[str, ManifestURL] = {} topic_parts = [] for m in manifests: if m.topic: topic_parts.append(m.topic) for url_entry in m.urls: normalized = normalize_url(url_entry.url) existing = seen.get(normalized) if existing is None or ( url_entry.credibility_score and (existing.credibility_score or 0) < url_entry.credibility_score ): seen[normalized] = url_entry urls = list(seen.values()) return Manifest( discovery_date=datetime.now().isoformat(), topic=" + ".join(topic_parts) if topic_parts else None, total_urls=len(urls), urls=urls, ) def dedup_manifest_urls(manifest: Manifest, existing_urls: set[str]) -> Manifest: """Remove URLs already in the existing set (normalized comparison).""" existing_normalized = {normalize_url(u) for u in existing_urls} filtered = [u for u in manifest.urls if normalize_url(u.url) not in existing_normalized] return Manifest( discovery_date=manifest.discovery_date, topic=manifest.topic, total_urls=len(filtered), urls=filtered, ) def read_crawl_result(path: Path) -> CrawlResult: """Read a crawl result JSON file.""" data = json.loads(path.read_text()) return CrawlResult(**data) def write_crawl_result(result: CrawlResult, path: Path) -> None: """Write a crawl result to a JSON file.""" path.parent.mkdir(parents=True, exist_ok=True) path.write_text(result.model_dump_json(indent=2)) def create_crawl_result( entries: list[dict], crawler: str = "firecrawl", ) -> CrawlResult: """Create a CrawlResult from a list of crawl entry dicts.""" docs = [CrawlResultEntry(**e) for e in entries] completed = [d for d in docs if d.status == "completed"] failed = [d for d in docs if d.status != "completed"] return CrawlResult( crawl_date=datetime.now().isoformat(), crawler_used=crawler, total_crawled=len(completed), total_failed=len(failed), documents=docs, )