"""Public professional-reference export assembly for the Jyotish API. This boundary performs no writing-agent, persistence, billing, or telemetry work. It validates the public export request, reuses the handler's one full-reading calculation, and delegates packet assembly/rendering to ``jyotish_engine``. """ from __future__ import annotations import copy from datetime import datetime from importlib import import_module from types import SimpleNamespace from typing import Any try: from scripts.reader_appendix_language import clean_reader_appendix_markdown from scripts.reader_dasha_applicability import render_dasha_applicability from scripts.report_density_packet import report_density_packet except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution path from reader_appendix_language import clean_reader_appendix_markdown from reader_dasha_applicability import render_dasha_applicability from report_density_packet import report_density_packet class ProfessionalReportReferenceInputError(ValueError): """The professional-reference request is outside the public contract.""" READER_MAIN_EDITION = "reader_main" REFERENCE_EDITION = "reference" REPORT_VERSION_READER = "pl9_personal_long_report.v3" def _normalize_edition(value: Any) -> str: if value is None or value == "": return REFERENCE_EDITION if not isinstance(value, str): raise ProfessionalReportReferenceInputError("edition must be a string") normalized = value.strip() if normalized in {"", REFERENCE_EDITION, "professional_reference"}: return REFERENCE_EDITION if normalized == READER_MAIN_EDITION: return READER_MAIN_EDITION raise ProfessionalReportReferenceInputError("edition must be reference or reader_main") def _normalize_format(value: Any) -> str: if value is None: return "json" if not isinstance(value, str): raise ProfessionalReportReferenceInputError("format must be json or markdown") normalized = value.strip().lower() if normalized not in {"json", "markdown"}: raise ProfessionalReportReferenceInputError("format must be json or markdown") return normalized def _normalize_packs(value: Any) -> list[str]: if value is None: return [] if isinstance(value, str): raw_items = value.split(",") elif isinstance(value, list): if any(not isinstance(item, str) for item in value): raise ProfessionalReportReferenceInputError("packs must contain only strings") raw_items = value else: raise ProfessionalReportReferenceInputError("packs must be a string or array of strings") selected: list[str] = [] seen: set[str] = set() for item in raw_items: pack_id = item.strip() if pack_id and pack_id not in seen: selected.append(pack_id) seen.add(pack_id) return selected def _normalize_languages(value: Any) -> list[str]: """``languages`` is optional; only ``zh`` and ``en`` exist. Absent means today's zh-only reply.""" if value is None: return [] if not isinstance(value, list) or any(not isinstance(item, str) for item in value): raise ProfessionalReportReferenceInputError("languages must be an array of strings") selected = [] for item in value: language = item.strip().lower() if language not in {"zh", "en"}: raise ProfessionalReportReferenceInputError("languages may contain only zh and en") if language not in selected: selected.append(language) return selected def _english_edition(packet: dict, include_fact_tables: bool) -> dict[str, Any]: """Render the English reader from a deep copy of the same packet. The English edition is secondary: any failure, or any Chinese character left in it, drops it and reports why. The Chinese edition is never touched. """ try: from scripts.pl9_reader_english import HAN, _public_wording_en from scripts.pl9_reader_export import _pl9_export_markdown_for_edition except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution path from pl9_reader_english import HAN, _public_wording_en from pl9_reader_export import _pl9_export_markdown_for_edition try: english = copy.deepcopy(packet) english["report_language"] = "en" markdown = _pl9_export_markdown_for_edition(english, READER_MAIN_EDITION) applicability = ( _public_wording_en(render_dasha_applicability(copy.deepcopy(packet), language="en")) if include_fact_tables else None ) except Exception: # noqa: BLE001 - the English edition must never fail the Chinese one return {"english_unavailable": "render_error"} if not isinstance(markdown, str) or not markdown.strip(): return {"english_unavailable": "empty"} if HAN.search(markdown) or (applicability is not None and HAN.search(applicability)): return {"english_unavailable": "han_leak"} return { "markdown_en": markdown, **({"reader_dasha_applicability_en": applicability} if applicability is not None else {}), } def _load_engine(): try: return import_module("scripts.jyotish_engine") except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution path return import_module("jyotish_engine") def _export_args(birth: dict[str, Any]) -> SimpleNamespace: today = birth.get("today") or datetime.now().strftime("%Y-%m-%d") raw_target = birth.get("target_year") if raw_target in (None, ""): target_year = int(str(today)[:4]) else: target_year = int(raw_target) raw_age = birth.get("age") if raw_age in (None, ""): try: age = int(target_year) - int(birth["year"]) except (TypeError, ValueError, KeyError): age = None else: age = int(raw_age) payload = { **birth, "hour": int(birth["hour"]), "minute": int(birth["minute"]), "second": int(birth.get("second", 0) or 0), "today": today, "target_year": target_year, "age": age, "visual_chart_observations": birth.get("visual_chart_observations"), "startrack_language_bridge": bool(birth.get("startrack_language_bridge")), } return SimpleNamespace(**payload) def build_professional_report_reference(handler, body: dict[str, Any], *, engine=None) -> dict[str, Any]: """Build one JSON or Markdown response from one reused full-reading result.""" if not isinstance(body, dict): raise ProfessionalReportReferenceInputError("request body must be an object") output_format = _normalize_format(body.get("format")) edition = _normalize_edition(body.get("edition")) packs = _normalize_packs(body.get("packs")) languages = _normalize_languages(body.get("languages")) if languages and (output_format != "markdown" or edition != READER_MAIN_EDITION): raise ProfessionalReportReferenceInputError("languages requires format markdown and edition reader_main") birth = handler._high_rigor_birth_payload(body) full_reading = handler._compute_full_reading_for_thematic(birth) resolved_engine = engine or _load_engine() try: packet = resolved_engine.build_professional_report_reference_packet( full_reading, _export_args(birth), packs, ) except ValueError as exc: raise ProfessionalReportReferenceInputError(str(exc)) from exc if edition == READER_MAIN_EDITION and isinstance(packet, dict): packet = dict(packet) packet["report_version"] = REPORT_VERSION_READER if output_format == "markdown": if edition == READER_MAIN_EDITION: try: from scripts.pl9_reader_export import _pl9_export_markdown_for_edition except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution path from pl9_reader_export import _pl9_export_markdown_for_edition # English renders from its own deep copy first, so nothing the Chinese # renderer does to the packet can reach it (and vice versa). english = _english_edition(packet, body.get("include_fact_tables") is True) if "en" in languages else {} markdown = _pl9_export_markdown_for_edition(packet, READER_MAIN_EDITION) else: markdown = resolved_engine.render_pl9_markdown(packet) return { "format": "markdown", "edition": edition, "report_version": REPORT_VERSION_READER if edition == READER_MAIN_EDITION else packet.get("report_version"), "markdown": markdown, **({"fact_table_packet": report_density_packet(packet), "reader_dasha_applicability": clean_reader_appendix_markdown(render_dasha_applicability(packet))} if body.get("include_fact_tables") is True else {}), **(english if edition == READER_MAIN_EDITION and "en" in languages else {}), } return { "format": "json", "edition": edition, "report": packet, }