"""funnies data model. Per functional-design entities (unit: funnies): FunniesBrief, Crossword, FindAWord, Cartoon, Comic, FunniesResult. Lightweight plain-data shapes owned by the funnies library; Contract 2/4. Status ready/fallback/empty keeps failure graceful (Q5=A / BR3.2). """ from __future__ import annotations from dataclasses import dataclass, field from typing import Literal FunniesStatus = Literal["ready", "fallback", "empty"] @dataclass class FunniesBrief: """Content-derived input funnies receives from the generator (Contract 4).""" theme_words: list[str] = field(default_factory=list) context: str = "" cartoon_source: str = "internet-allow" @dataclass class Crossword: """A single AI-drafted crossword, laid out on a 10x10 grid (Q2=B).""" crossword_id: str grid: list[list[str]] clues_across: list[str] clues_down: list[str] solution: list[list[str]] @dataclass class FindAWord: """A find-a-word puzzle built from theme words (BR1.3).""" find_id: str words: list[str] grid: list[list[str]] @dataclass class Cartoon: """An embedded single-panel cartoon (xkcd or generated fallback, Q3=A).""" cartoon_id: str source: str # xkcd | generated | none art_ref: str # local/bundled asset (never a remote URL at print; NFR4) caption: str = "" @dataclass class Comic: """A comic strip in the funnies section (Q4=A, BR3.1).""" comic_id: str source: str art_ref: str # local/bundled dialogue: list[str] = field(default_factory=list) @dataclass class FunniesResult: """The container funnies returns to Generator (Contract 2). Graceful fallback/empty handling on no viable content (Q5=A / BR3.2).""" funnies_result_id: str crossword: Crossword | None = None find_a_word: FindAWord | None = None cartoons: list[Cartoon] = field(default_factory=list) comics: list[Comic] = field(default_factory=list) status: FunniesStatus = "ready" status_message: str = "" def is_self_contained(self) -> bool: """BR4.1 / NFR4: no remote URL survives in any art_ref.""" for art in self.cartoons + self.comics: if art.art_ref.startswith(("http://", "https://")): return False return True