"""render_automatic_eda_pdf — chapter-based EDA report as an A5-portrait PDF. Public ``eda``-group entry point of the AutomaticEDA engine. Takes either a list of chapters (the format-independent document model) or an ``eda`` TableProfile dict (in which case the canonical chapters are built with ``build_document``), and renders a mobile-first PDF whose paginator MEASURES every block and never cuts text, tables or images: text wraps to whole lines, long tables split by rows repeating the header, figures/images scale to fit entirely. Each chapter starts on a fresh page stamped `` · v`` in the footer, and a per-chapter manifest (``automatic_eda_manifest.json``) is written next to the output for version tracking. dict-no-throw: never raises. Returns ``{path, n_pages, chapters, manifest_path, note}``; on a fatal write error ``path`` is None and ``note`` explains why. Additive: this does NOT replace ``render_eda_pdf`` (still used by ``profile_table(emit_pdf=True)``). It is the new engine that will, in the next phase, let every EDA emit both a PDF and a PPTX from the same chapter model. """ from __future__ import annotations import os from datascience.automatic_eda import build_document, merge_manifest, render_pdf from datascience.automatic_eda.model import as_chapter, as_chapters def _coerce_chapters(chapters_or_profile, meta: dict) -> list: """Accept chapters OR an eda profile and return a list of Chapter.""" arg = chapters_or_profile if isinstance(arg, (list, tuple)): return as_chapters(list(arg)) if isinstance(arg, dict): # A single chapter dict has 'blocks'; a profile has columns/table/rows. if "blocks" in arg and "columns" not in arg: ch = as_chapter(arg) return [ch] if ch is not None else [] # Treat as an eda TableProfile. return build_document(arg, (meta or {}).get("ctx")) return [] def render_automatic_eda_pdf(chapters_or_profile, out_path: str, meta: dict = None) -> dict: """Render an AutomaticEDA document into a mobile-readable PDF. Args: chapters_or_profile: either a list of chapters (``Chapter`` dataclasses or dicts following the document model) or an ``eda`` TableProfile dict — in the latter case the canonical chapters are built via ``build_document(profile, meta['ctx'])``. out_path: filesystem path for the PDF (parent dirs are created). meta: optional dict. Recognised keys: ``title`` (cover/footer title), ``ctx`` (presentation context passed to chapter builders when a profile is given), ``manifest_path`` (override; defaults to ``automatic_eda_manifest.json`` beside ``out_path``), ``write_manifest`` (set False to skip), ``generated_at``. Returns: dict (never raises): ``{path, n_pages, chapters, manifest_path, note}``. """ meta = dict(meta or {}) chapters = _coerce_chapters(chapters_or_profile, meta) result = render_pdf(chapters, out_path, meta) manifest_path = None if meta.get("write_manifest", True) and result.get("path"): manifest_path = meta.get("manifest_path") if not manifest_path: manifest_path = os.path.join( os.path.dirname(os.path.abspath(out_path)), "automatic_eda_manifest.json") generated_at = meta.get("generated_at") or _now_iso() merge_manifest(manifest_path, "pdf", result.get("chapters") or [], generated_at) result["manifest_path"] = manifest_path return result def _now_iso() -> str: from datetime import datetime, timezone return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")