- /api/professional_report_reference accepts languages [zh,en]; one packet, zh byte-identical, markdown_en + reader_dasha_applicability_en, or english_unavailable when a Chinese character would leak. - pl9_reader_english renders the parity volume with report_language=en and a fixed term table; yoga_engine carries combination_en / effects_en / strength_en. - 75 fictional charts: zero Han, same line count, same numbers outside yoga tables. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
218 lines
9.0 KiB
Python
218 lines
9.0 KiB
Python
"""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,
|
|
}
|