The list query had been split into four reads, so older pages no longer followed one cursor. A7 was read from the wrong module, and the shell asked for the product switch on every mount. Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor <cursoragent@cursor.com>
226 lines
9.2 KiB
Python
226 lines
9.2 KiB
Python
"""Glue around the upstream full-synastry skill.
|
|
|
|
Two birth payloads become two native full-readings, then the untouched
|
|
``build_full_synastry_dossier``. Raman is applied to the snapshots the skill
|
|
already extracted. This module does not edit those upstream files.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
from typing import Any, Callable
|
|
|
|
from marriage_match import build_marriage_match_dossier
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
DOSSIER_SCRIPT = (
|
|
ROOT
|
|
/ "skills"
|
|
/ "jyotish-full-synastry-reading"
|
|
/ "scripts"
|
|
/ "build_full_synastry_dossier.py"
|
|
)
|
|
RULES_VERSION = "bde5d615:marriage_match_product_v1"
|
|
RAMAN_PROFILE = "raman_muhurtha_v1"
|
|
|
|
ReadingFn = Callable[[dict[str, Any]], dict[str, Any]]
|
|
|
|
|
|
def _dossier_builder():
|
|
spec = importlib.util.spec_from_file_location(
|
|
"build_full_synastry_dossier",
|
|
DOSSIER_SCRIPT,
|
|
)
|
|
if spec is None or spec.loader is None:
|
|
raise RuntimeError("full synastry dossier script is missing")
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module.build_full_synastry_dossier
|
|
|
|
|
|
def _parse_day(value: Any) -> datetime | None:
|
|
if not isinstance(value, str) or not value.strip():
|
|
return None
|
|
text = value.strip()[:10]
|
|
try:
|
|
return datetime.strptime(text, "%Y-%m-%d")
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
def _clip_period(period: dict[str, Any], level: str, window_start: datetime, window_end: datetime) -> dict[str, Any] | None:
|
|
start = _parse_day(period.get("start") or period.get("start_datetime"))
|
|
end = _parse_day(period.get("end") or period.get("end_datetime"))
|
|
if start is None or end is None or end <= window_start or start >= window_end:
|
|
return None
|
|
lord = period.get("lord")
|
|
if not isinstance(lord, str) or not lord:
|
|
return None
|
|
lord_cn = period.get("lord_cn") if isinstance(period.get("lord_cn"), str) else lord
|
|
return {
|
|
"level": level,
|
|
"lord": lord,
|
|
"lordCn": lord_cn,
|
|
"start": max(start, window_start).strftime("%Y-%m-%d"),
|
|
"end": min(end, window_end).strftime("%Y-%m-%d"),
|
|
}
|
|
|
|
|
|
def upcoming_vimshottari(reading: dict[str, Any], today: str, years: int = 5) -> list[dict[str, Any]]:
|
|
"""Mahadasha and antardasha slices that overlap the next few years."""
|
|
packet = reading.get("raw_full_reading") if isinstance(reading.get("raw_full_reading"), dict) else reading
|
|
modules = packet.get("modules") if isinstance(packet, dict) else {}
|
|
dasha = modules.get("dasha") if isinstance(modules, dict) else {}
|
|
timeline = dasha.get("timeline") if isinstance(dasha, dict) else []
|
|
start = _parse_day(today) or datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
end = start + timedelta(days=365 * years)
|
|
rows: list[dict[str, Any]] = []
|
|
if not isinstance(timeline, list):
|
|
return rows
|
|
for mahadasha in timeline:
|
|
if not isinstance(mahadasha, dict):
|
|
continue
|
|
clipped = _clip_period(mahadasha, "md", start, end)
|
|
if clipped:
|
|
rows.append(clipped)
|
|
for antardasha in mahadasha.get("antardasha_timeline") or []:
|
|
if not isinstance(antardasha, dict):
|
|
continue
|
|
child = _clip_period(antardasha, "ad", start, end)
|
|
if child:
|
|
rows.append(child)
|
|
return rows
|
|
|
|
|
|
_SIGNS = (
|
|
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
|
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
|
)
|
|
_SIGN_LORDS = {
|
|
"Aries": "Mars", "Taurus": "Venus", "Gemini": "Mercury", "Cancer": "Moon",
|
|
"Leo": "Sun", "Virgo": "Mercury", "Libra": "Venus", "Scorpio": "Mars",
|
|
"Sagittarius": "Jupiter", "Capricorn": "Saturn", "Aquarius": "Saturn", "Pisces": "Jupiter",
|
|
}
|
|
|
|
|
|
def _reading_packet(reading: dict[str, Any]) -> dict[str, Any]:
|
|
nested = reading.get("raw_full_reading")
|
|
return nested if isinstance(nested, dict) else reading
|
|
|
|
|
|
def _sign_name(value: Any) -> str | None:
|
|
if isinstance(value, str) and value.strip():
|
|
return value.strip()
|
|
if not isinstance(value, dict):
|
|
return None
|
|
for key in ("sign", "sign_name", "rasi"):
|
|
sign = value.get(key)
|
|
if isinstance(sign, str) and sign.strip():
|
|
return sign.strip()
|
|
for key in ("longitude", "degree_raw", "lon", "degree"):
|
|
longitude = value.get(key)
|
|
if isinstance(longitude, (int, float)) and not isinstance(longitude, bool):
|
|
return _SIGNS[int(float(longitude) % 360.0) // 30]
|
|
return None
|
|
|
|
|
|
def _named_planet(root: Any, path: tuple[str, ...]) -> str | None:
|
|
value = root
|
|
for key in path:
|
|
value = value.get(key) if isinstance(value, dict) else None
|
|
if isinstance(value, str) and value.strip():
|
|
return value.strip()
|
|
if isinstance(value, dict):
|
|
planet = value.get("planet") or value.get("name")
|
|
if isinstance(planet, str) and planet.strip():
|
|
return planet.strip()
|
|
return None
|
|
|
|
|
|
def relationship_structure(reading: dict[str, Any]) -> dict[str, Any]:
|
|
"""Signs and relationship points the longitude-only snapshot does not carry."""
|
|
modules = _reading_packet(reading).get("modules")
|
|
modules = modules if isinstance(modules, dict) else {}
|
|
chart = modules.get("chart") if isinstance(modules.get("chart"), dict) else {}
|
|
planets = chart.get("planets") if isinstance(chart.get("planets"), dict) else {}
|
|
ascendant = _sign_name(chart.get("ascendant"))
|
|
seventh = _SIGN_LORDS[_SIGNS[(_SIGNS.index(ascendant) + 6) % 12]] if ascendant in _SIGN_LORDS else None
|
|
jaimini = modules.get("jaimini") if isinstance(modules.get("jaimini"), dict) else {}
|
|
darakaraka = (
|
|
_named_planet(jaimini, ("chara_karakas", "DK"))
|
|
or _named_planet(jaimini, ("chara_karakas", "Darakaraka"))
|
|
or _named_planet(jaimini, ("chara_karaka_7", "karaka_table", "Darakaraka"))
|
|
or _named_planet(jaimini, ("chara_karaka_8", "karaka_table", "Darakaraka"))
|
|
)
|
|
special = modules.get("special_lagnas") if isinstance(modules.get("special_lagnas"), dict) else {}
|
|
if not special and isinstance(jaimini.get("special_lagnas"), dict):
|
|
special = jaimini.get("special_lagnas")
|
|
arudha = jaimini.get("arudha_padas") if isinstance(jaimini.get("arudha_padas"), dict) else {}
|
|
if not arudha and isinstance(modules.get("arudha_padas"), dict):
|
|
arudha = modules.get("arudha_padas")
|
|
padas = arudha.get("padas") if isinstance(arudha.get("padas"), dict) else arudha
|
|
upapada = _sign_name(special.get("Upapada_Lagna") or special.get("UL") or arudha.get("upapada"))
|
|
if upapada is None and isinstance(padas, dict):
|
|
upapada = _sign_name(padas.get("UL"))
|
|
a7 = _sign_name(padas.get("A7") if isinstance(padas, dict) else None)
|
|
if a7 is None:
|
|
a7 = _sign_name(special.get("A7") or special.get("Darapada") or arudha.get("A7"))
|
|
vargas = modules.get("varga_full") if isinstance(modules.get("varga_full"), dict) else {}
|
|
navamsa = vargas.get("D9_Navamsa") if isinstance(vargas.get("D9_Navamsa"), dict) else {}
|
|
d9 = _sign_name(navamsa.get("Ascendant") or navamsa.get("ascendant"))
|
|
return {
|
|
"ascendant": {"sign": ascendant} if ascendant else {},
|
|
"planets": {
|
|
"Venus": {"sign": _sign_name(planets.get("Venus"))} if _sign_name(planets.get("Venus")) else {},
|
|
"Moon": {"sign": _sign_name(planets.get("Moon"))} if _sign_name(planets.get("Moon")) else {},
|
|
},
|
|
"seventh_lord": seventh,
|
|
"darakaraka": darakaraka,
|
|
"upapada": upapada,
|
|
"A7": a7,
|
|
"d9": {"ascendant": d9} if d9 else {},
|
|
}
|
|
|
|
|
|
def build_synastry_full_dossier(
|
|
handler: Any,
|
|
person_a: dict[str, Any],
|
|
person_b: dict[str, Any],
|
|
*,
|
|
read_chart: ReadingFn | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Run two full-readings in parallel, then both marriage-match profiles."""
|
|
read = read_chart or handler._compute_full_reading_for_thematic
|
|
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
future_a = pool.submit(read, person_a)
|
|
future_b = pool.submit(read, person_b)
|
|
reading_a = future_a.result()
|
|
reading_b = future_b.result()
|
|
dossier = _dossier_builder()(reading_a, reading_b)
|
|
if isinstance(dossier, dict):
|
|
dossier["local_structures"] = {
|
|
"person_a": relationship_structure(reading_a if isinstance(reading_a, dict) else {}),
|
|
"person_b": relationship_structure(reading_b if isinstance(reading_b, dict) else {}),
|
|
}
|
|
today = str(person_a.get("today") or person_b.get("today") or "")
|
|
snapshots = dossier.get("chart_snapshots") if isinstance(dossier, dict) else {}
|
|
snapshots = snapshots if isinstance(snapshots, dict) else {}
|
|
raman = build_marriage_match_dossier(
|
|
snapshots.get("person_a") or {},
|
|
snapshots.get("person_b") or {},
|
|
profile_id=RAMAN_PROFILE,
|
|
)
|
|
return {
|
|
"rules_version": RULES_VERSION,
|
|
"dossier": dossier,
|
|
"raman_marriage_match": raman,
|
|
"timing_periods": {
|
|
"person_a": upcoming_vimshottari(reading_a if isinstance(reading_a, dict) else {}, today),
|
|
"person_b": upcoming_vimshottari(reading_b if isinstance(reading_b, dict) else {}, today),
|
|
},
|
|
}
|