Перейти к содержанию

ranklens.report

ranklens.report

Reports: a complete HTML or Markdown file, written atomically.

AtomicReport

with AtomicReport(path) as fh: fh.write(...) — all of it or nothing.

Source code in src/ranklens/report/atomic.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
class AtomicReport:
    """``with AtomicReport(path) as fh: fh.write(...)`` — all of it or nothing."""

    def __init__(self, path: str | Path) -> None:
        self.path = Path(path)
        self._tmp: Path | None = None
        self._fh: IO[str] | None = None

    def __enter__(self) -> IO[str]:
        fd, tmp = tempfile.mkstemp(dir=self.path.parent, prefix=f".{self.path.name}.")
        self._tmp = Path(tmp)
        self._fh = os.fdopen(fd, "w", encoding="utf-8")
        return self._fh

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc: BaseException | None,
        tb: TracebackType | None,
    ) -> Literal[False]:
        assert self._fh is not None, "used outside a with block"
        assert self._tmp is not None, "used outside a with block"
        try:
            if exc_type is None:
                self._fh.flush()
                os.fsync(self._fh.fileno())
            self._fh.close()
            if exc_type is None:
                self._tmp.chmod(_mode_of(self.path))
                self._tmp.replace(self.path)
        finally:
            if not self._fh.closed:  # flush or fsync failed: still close before unlinking
                self._fh.close()
            if self._tmp.exists():
                self._tmp.unlink()
        return False

Section dataclass

table is a list of rows with the same keys; figure a plotly Figure.

Source code in src/ranklens/report/sections.py
10
11
12
13
14
15
16
17
@dataclass(frozen=True)
class Section:
    """``table`` is a list of rows with the same keys; ``figure`` a plotly Figure."""

    title: str
    text: str = ""
    table: Sequence[Mapping[str, object]] = field(default_factory=tuple)
    figure: Any = None

render_html

render_html(title: str, sections: Sequence[Section]) -> str

The whole document as a string.

Source code in src/ranklens/report/html.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def render_html(title: str, sections: Sequence[Section]) -> str:
    """The whole document as a string."""
    parts = [
        "<!DOCTYPE html>",
        '<html lang="en"><head><meta charset="utf-8">',
        # the charts are drawn on white: a dark browser theme must not darken the page
        '<meta name="color-scheme" content="light">',
        f"<title>{html.escape(title)}</title><style>{_STYLE}</style></head><body>",
        f"<h1>{html.escape(title)}</h1>",
    ]
    plotly_embedded = False
    for section in sections:
        parts.append(f"<h2>{html.escape(section.title)}</h2>")
        if section.text:
            parts.append(f'<p class="note">{html.escape(section.text)}</p>')
        if section.table:
            parts.append(_table(section))
        if section.figure is not None:
            parts.append(
                section.figure.to_html(
                    full_html=False, include_plotlyjs=not plotly_embedded, default_width="100%"
                )
            )
            plotly_embedded = True
    parts.append("</body></html>")
    return "\n".join(parts)

write_html

write_html(
    path: str | Path,
    title: str,
    sections: Sequence[Section],
) -> None

Render and write atomically: the file is the old one or the complete new one.

Source code in src/ranklens/report/html.py
55
56
57
58
59
def write_html(path: str | Path, title: str, sections: Sequence[Section]) -> None:
    """Render and write atomically: the file is the old one or the complete new one."""
    document = render_html(title, sections)
    with AtomicReport(path) as fh:
        fh.write(document)

write_report

write_report(
    path: str | Path,
    title: str,
    sections: Sequence[Section],
) -> None

HTML for .html/.htm, Markdown for .md; anything else is an error.

Source code in src/ranklens/report/__init__.py
22
23
24
25
26
27
28
29
30
def write_report(path: str | Path, title: str, sections: Sequence[Section]) -> None:
    """HTML for ``.html``/``.htm``, Markdown for ``.md``; anything else is an error."""
    suffix = Path(path).suffix.lower()
    if suffix in (".html", ".htm"):
        write_html(path, title, sections)
    elif suffix == ".md":
        write_markdown(path, title, sections)
    else:
        raise ValueError(f"{path}: a report is .html or .md, got {suffix or 'no extension'!r}")