"""Map v2 API JSON to ViewData. This is the only module that reads API field names. Contract (v2/playground/README.md "Response contract"): status, answer, claims[] {text, quote, slug, verified, mm:ss, ...}, triage[] {slug, label, reason}, warnings[], plus the public API's episodes {slug: {title, date, url}}. Unknown fields are ignored. """ from __future__ import annotations from dataclasses import dataclass STATUSES = ("answered", "partial", "not_in_podcast", "false_premise", "refused", "error") @dataclass(frozen=True) class EpisodeView: slug: str title: str date: str url: str @dataclass(frozen=True) class ClaimView: text: str quote: str verified: bool episode: EpisodeView timestamp: str @dataclass(frozen=True) class TriageView: episode: EpisodeView label: str reason: str @dataclass(frozen=True) class ViewData: status: str answer_markdown: str claims: tuple[ClaimView, ...] sources: tuple[EpisodeView, ...] # cited episodes, first-cited order, unique triage: tuple[TriageView, ...] warnings: tuple[str, ...] @property def is_error(self) -> bool: return self.status == "error" def _s(value: object) -> str: return value if isinstance(value, str) else "" def map_api_response(data: object) -> ViewData: if not isinstance(data, dict): return ViewData("error", "", (), (), (), ()) status = _s(data.get("status")) if status not in STATUSES: status = "error" raw_eps = data.get("episodes") if isinstance(data.get("episodes"), dict) else {} def episode(slug: str) -> EpisodeView: meta = raw_eps.get(slug) if isinstance(raw_eps.get(slug), dict) else {} return EpisodeView(slug, _s(meta.get("title")), _s(meta.get("date")), _s(meta.get("url"))) claims: list[ClaimView] = [] for item in data.get("claims") if isinstance(data.get("claims"), list) else []: if not isinstance(item, dict): continue claims.append(ClaimView( text=_s(item.get("text")), quote=_s(item.get("quote")), # Only an explicit true counts as verified. verified=item.get("verified") is True, episode=episode(_s(item.get("slug"))), timestamp=_s(item.get("mm:ss")), )) sources: dict[str, EpisodeView] = {} for claim in claims: if claim.episode.slug: sources.setdefault(claim.episode.slug, claim.episode) triage = tuple( TriageView(episode(_s(t.get("slug"))), _s(t.get("label")), _s(t.get("reason"))) for t in (data.get("triage") if isinstance(data.get("triage"), list) else []) if isinstance(t, dict) ) warnings = tuple(str(w) for w in (data.get("warnings") if isinstance(data.get("warnings"), list) else []) if str(w).strip()) return ViewData(status, _s(data.get("answer")), tuple(claims), tuple(sources.values()), triage, warnings)