"""HTTP-facing marriage match and full synastry dossier. The dossier endpoint caches on the two birth payloads plus ``GITHUB_SHA``, the same scratch helper the report reference cache uses. A missing or ``unknown`` commit id skips the cache. """ from __future__ import annotations import os from typing import Any from marriage_match import build_marriage_match_dossier from marriage_match_service.full_dossier import RULES_VERSION, build_synastry_full_dossier try: from scripts import consult_full_data_cache as _cache except ModuleNotFoundError: # pragma: no cover - direct scripts/ execution import consult_full_data_cache as _cache CACHE_NAME = "synastry_full_dossier_cache" _BIRTH_KEYS = ("year", "month", "day", "hour", "minute", "lat", "lon", "tz") class MarriageMatchInputError(ValueError): """The caller sent a shape this product does not accept.""" def compute_marriage_match(payload: dict[str, Any]) -> dict[str, Any]: """Snapshot-only product. Birth details are not accepted here.""" if not isinstance(payload, dict): raise MarriageMatchInputError("request body must be an object") person_a = payload.get("person_a") person_b = payload.get("person_b") if not isinstance(person_a, dict) or not isinstance(person_b, dict): raise MarriageMatchInputError("person_a and person_b must be chart snapshots") profile_id = payload.get("profile_id") or "practitioner_consensus_v1" if not isinstance(profile_id, str) or not profile_id.strip(): raise MarriageMatchInputError("profile_id must be a string") try: return build_marriage_match_dossier( person_a, person_b, profile_id=profile_id.strip(), ) except ValueError as exc: raise MarriageMatchInputError(str(exc)) from exc def _birth(value: Any, label: str) -> dict[str, Any]: if not isinstance(value, dict): raise MarriageMatchInputError(f"{label} must be an object") missing = [key for key in _BIRTH_KEYS if value.get(key) is None] if missing: raise MarriageMatchInputError(f"{label} is missing {', '.join(missing)}") return value def _birth_identity(body: dict[str, Any]) -> dict[str, Any]: node = body.get("node_mode", body.get("nodeMode", "mean")) return { "year": body.get("year"), "month": body.get("month"), "day": body.get("day"), "hour": body.get("hour"), "minute": body.get("minute"), "second": body.get("second", 0), "lat": body.get("lat"), "lon": body.get("lon"), "tz": body.get("tz"), "ayanamsa": body.get("ayanamsa", "lahiri"), "node_mode": node or "mean", } def _commit_sha() -> str: return os.environ.get("GITHUB_SHA") or "" def check_synastry_reader(body: dict[str, Any]) -> dict[str, Any]: """Relationship-language gate. The body is the delivery plus the section inputs.""" if not isinstance(body, dict): raise MarriageMatchInputError("request body must be an object") from marriage_match_service.synastry_full_reader_gate import validate_synastry_reader delivery = body.get("delivery") section_inputs = body.get("sectionInputs") language = body.get("language") or "zh" if not isinstance(delivery, dict) or not isinstance(language, str): raise MarriageMatchInputError("delivery and language are required") if section_inputs is not None and not isinstance(section_inputs, dict): raise MarriageMatchInputError("sectionInputs must be an object") return validate_synastry_reader(delivery, section_inputs or {}, language) def compute_synastry_full_dossier(handler: Any, body: dict[str, Any]) -> dict[str, Any]: if not isinstance(body, dict): raise MarriageMatchInputError("request body must be an object") person_a = _birth(body.get("person_a"), "person_a") person_b = _birth(body.get("person_b"), "person_b") sha = _commit_sha() def build(_identity: dict[str, Any]) -> dict[str, Any]: return build_synastry_full_dossier(handler, person_a, person_b) if sha in ("", "unknown"): return build({}) cached = _cache.get_or_build( { "person_a": _birth_identity(person_a), "person_b": _birth_identity(person_b), "github_sha": sha, "rules_version": RULES_VERSION, }, build, cache_name=CACHE_NAME, ) packet = cached["packet"] if not isinstance(packet, dict): raise MarriageMatchInputError("cached dossier was not an object") return packet