"""HTTP dasha registry. `DASHA_REGISTRY_METADATA` is the old `extended_dashas.DASHA_REGISTRY` moved verbatim so `/api/chart` `available_dashas` stays byte-identical. `compute_dasha` calls the same modules `cmd_full_reading` calls, with the same birth-info fields. Systems without that module return "not implemented" instead of a fabricated timeline. This module must not import `jyotish_api_server` or PyJHora. """ from __future__ import annotations from datetime import datetime from typing import Any try: from source_bounded_child_periods import PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE except ModuleNotFoundError: # pragma: no cover - package import from scripts.source_bounded_child_periods import PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE DASHA_REGISTRY_METADATA = { "vimshottari": {"name": "Vimshottari", "years": 120, "type": "nakshatra"}, "ashtottari": {"name": "Ashtottari", "years": 108, "type": "nakshatra"}, "yogini": {"name": "Yogini", "years": 36, "type": "nakshatra"}, "kalachakra": {"name": "Kalachakra", "years": 94, "type": "nakshatra"}, "tara": {"name": "Tara Dasha", "years": 81, "type": "nakshatra"}, "shodasottari": {"name": "Shodasottari", "years": 128, "type": "nakshatra"}, "panchottari": {"name": "Panchottari", "years": 105, "type": "nakshatra"}, "satabdika": {"name": "Satabdika", "years": 100, "type": "nakshatra"}, "chara": {"name": "Chara (Jaimini)", "years": 36, "type": "rasi"}, "narayana": {"name": "Narayana", "years": 36, "type": "rasi"}, "shoola": {"name": "Shoola Dasha", "years": 9, "type": "rasi"}, "sudasa": {"name": "Sudasa", "years": 36, "type": "rasi"}, "drig": {"name": "Drig Dasha", "years": 36, "type": "rasi"}, "sthira": {"name": "Sthira Dasha", "years": 36, "type": "rasi"}, "mandooka": {"name": "Mandooka Dasha", "years": 36, "type": "rasi"}, "lagnamsaka": {"name": "Lagnamsaka Dasha", "years": 36, "type": "rasi"}, "yogardha": {"name": "Yogardha Dasha", "years": 36, "type": "rasi"}, "brahma": {"name": "Brahma Dasha", "years": 36, "type": "rasi"}, "dwisaptati": {"name": "Dwisaptati Sama", "years": 72, "type": "conditional"}, "shattrimsa": {"name": "Shattrimsa Sama", "years": 36, "type": "conditional"}, "dwadashottari": {"name": "Dwadashottari", "years": 112, "type": "conditional"}, "shasti_hayani": {"name": "Shasti-Hayani", "years": 60, "type": "conditional"}, "chaturaaseeti": {"name": "Chaturaaseeti Sama", "years": 84, "type": "conditional"}, "tithi_ashtottari": {"name": "Tithi Ashtottari", "years": 108, "type": "conditional"}, "tithi_yogini": {"name": "Tithi Yogini", "years": 36, "type": "conditional"}, "patyayini": {"name": "Patyayini", "years": 36, "type": "conditional"}, "navamsa": {"name": "Navamsa Dasha", "years": 36, "type": "varga"}, "kendradi": {"name": "Kendradi", "years": 38, "type": "bhav"}, "paryaaya": {"name": "Paryaaya Dasha", "years": 36, "type": "varga"}, "rashmi": {"name": "Rashmi Dasha", "years": 36, "type": "special"}, "naisargika": {"name": "Naisargika Dasha", "years": 120, "type": "special"}, "aayu": {"name": "Aayu Dasha", "years": 36, "type": "special"}, "karaka": {"name": "Karaka Dasha", "years": 36, "type": "special"}, "panchasvara": {"name": "Panchasvara Dasha", "years": 36, "type": "special"}, "sudarsana": {"name": "Sudarsana Chakra Dasha", "years": 36, "type": "special"}, } _CONDITIONAL_CALLS = { "shodasottari": ("shodashottari_dasha", "calculate_shodashottari_dasha", {}), "dwadashottari": ( "dwadashottari_dasha", "calculate_dwadashottari_dasha", {"sukramsaka_profile": "navamsa_lagna_taurus_or_libra"}, ), "dwisaptati": ("dwisaptatisama_dasha", "calculate_dwisaptatisama_dasha", {}), "shattrimsa": ("shattrimshatsama_dasha", "calculate_shattrimshatsama_dasha", {}), "panchottari": ("panchottari_dasha", "calculate_panchottari_dasha", {}), "satabdika": ("satabdika_dasha", "calculate_satabdika_dasha", {}), "chaturaaseeti": ("chaturshitisama_dasha", "calculate_chaturshitisama_dasha", {}), } 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", } class DashaNotImplemented(Exception): """The registry key exists, but there is no engine module behind it.""" def available_dasha_rows() -> list[dict[str, Any]]: """`/api/chart` `available_dashas` rows, same order and fields as before.""" return [ {"key": key, "name": info["name"], "years": info["years"], "type": info["type"]} for key, info in DASHA_REGISTRY_METADATA.items() ] def _load(module_name: str): try: return __import__(module_name) except ModuleNotFoundError: # pragma: no cover - package import return __import__(f"scripts.{module_name}", fromlist=[module_name]) def _longitude(value: Any) -> float | None: if isinstance(value, (int, float)): return float(value) % 360.0 if not isinstance(value, dict): return None for key in ("lon", "longitude", "degree"): raw = value.get(key) if isinstance(raw, (int, float)): return float(raw) % 360.0 sign_idx = value.get("sign_idx") if sign_idx is None and value.get("sign") in SIGNS: sign_idx = SIGNS.index(value["sign"]) degree = value.get("degree_in_sign", value.get("degree", 0)) try: if sign_idx is None: return None return (int(sign_idx) * 30 + float(degree or 0)) % 360.0 except (TypeError, ValueError): return None def _planet_longitudes(planets: dict) -> dict[str, float]: longitudes = {} if not isinstance(planets, dict): return longitudes for name, data in planets.items(): longitude = _longitude(data) if longitude is not None: longitudes[str(name)] = longitude return longitudes def _optional_float(body: dict, *keys: str) -> float | None: for key in keys: raw = body.get(key) if isinstance(raw, (int, float)): return float(raw) return None def _reference_datetime(birth_dt: datetime, body: dict) -> datetime: raw = body.get("today") or body.get("current_date") if isinstance(raw, datetime): return raw if isinstance(raw, str) and raw.strip(): return datetime.strptime(raw.strip()[:10], "%Y-%m-%d") return datetime.now() def build_reading_birth_info( birth_dt: datetime, planets: dict, ascendant: dict | None, body: dict | None = None, ) -> tuple[dict[str, Any], dict[str, Any], dict[str, float]]: """Same fields `cmd_full_reading` puts in `conditional_birth_info`. Returns `(alt_dasha_info, conditional_birth_info, planet_longitudes)`. Kept here, not extracted from the engine, so the full-reading dict construction and its output stay untouched. """ body = body or {} ascendant = ascendant or {} planet_lons = _planet_longitudes(planets) moon_lon = planet_lons.get("Moon", _longitude(body.get("moon_lon")) or 0.0) sun_lon = planet_lons.get("Sun", _longitude(body.get("sun_lon")) or 0.0) asc_lon = _longitude(ascendant) if asc_lon is None: asc_lon = _longitude(body.get("ascendant_longitude")) if asc_lon is None: sign_idx = ascendant.get("sign_idx", body.get("asc_sign_idx", 0)) try: asc_idx = int(sign_idx) % 12 except (TypeError, ValueError): asc_idx = 0 asc_lon = float(asc_idx * 30) asc_idx = int(float(asc_lon) / 30) % 12 moon_nakshatra_index = int(moon_lon / (360 / 27)) % 27 moon_pada = int((moon_lon % (360 / 27)) / (360 / 108)) + 1 tithi_number = int(((moon_lon - sun_lon) % 360) / 12) + 1 lagna_lord_name = SIGN_LORDS[SIGNS[asc_idx]] tenth_lord_name = SIGN_LORDS[SIGNS[(asc_idx + 9) % 12]] try: varga = _load("varga") navamsa_ascendant_sign_index = int(varga.calc_varga(asc_lon, 9).get("sign_idx", 0)) % 12 venus_navamsa_sign_index = int(varga.calc_varga(float(planet_lons.get("Venus", 0)), 9).get("sign_idx", 0)) % 12 except Exception: navamsa_ascendant_sign_index = None venus_navamsa_sign_index = None degree_in_sign = float(asc_lon) % 30.0 ascendant_hora_lord = ( "Sun" if (asc_idx % 2 == 0 and degree_in_sign < 15.0) or (asc_idx % 2 == 1 and degree_in_sign >= 15.0) else "Moon" ) birth_is_daytime = None lat = _optional_float(body, "lat", "latitude") lon = _optional_float(body, "lon", "longitude") tz = _optional_float(body, "tz", "timezone") if lat is not None and lon is not None and tz is not None: try: daynight = _load("saham_daynight") birth_is_daytime = bool(daynight.determine_daytime(birth_dt, lat=lat, lon=lon, tz=tz).get("is_daytime")) except Exception: birth_is_daytime = None applicability_rule_family = ( "rahu_from_lagna_lord_kendra_trikona_plus_paksha_daynight" if birth_is_daytime is not None else "moon_nakshatra_plus_paksha" ) alt = { "birth_datetime": birth_dt, "moon_nakshatra_index": moon_nakshatra_index, "moon_pada": moon_pada, "is_shukla_paksha": 1 <= tithi_number <= 15, "lagna_rashi_index": asc_idx, } conditional = { **alt, "ascendant_longitude": asc_lon, "sun_longitude": sun_lon, "moon_longitude": moon_lon, "birth_is_daytime": birth_is_daytime, "ascendant_sign_index": asc_idx, "ascendant_hora_lord": ascendant_hora_lord, "navamsa_ascendant_sign_index": navamsa_ascendant_sign_index, "venus_rasi_sign_index": int(float(planet_lons.get("Venus", 0)) / 30) % 12, "venus_navamsa_sign_index": venus_navamsa_sign_index, "tenth_lord_sign_index": int(float(planet_lons.get(tenth_lord_name, 0)) / 30) % 12, "rahu_sign_index": int(float(planet_lons.get("Rahu", 0)) / 30) % 12, "lagna_lord_sign_index": int(float(planet_lons.get(lagna_lord_name, 0)) / 30) % 12, "applicability_rule_family": applicability_rule_family, } return alt, conditional, planet_lons def _major_rows(payload: Any) -> list: if isinstance(payload, dict): majors = payload.get("major") if isinstance(majors, list): return majors periods = payload.get("periods") if isinstance(periods, list): return periods sequence = payload.get("mahadasha_sequence") if isinstance(sequence, list): return sequence return [] if isinstance(payload, list): return payload return [] def _conditional_payload(key: str, conditional: dict) -> dict: module_name, function_name, extra = _CONDITIONAL_CALLS[key] module = _load(module_name) birth_info = { **conditional, **extra, "child_period_profile": PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE, } return getattr(module, function_name)(birth_info) def compute_dasha( key: str, *, birth_dt: datetime, planets: dict, ascendant: dict | None, body: dict | None = None, ) -> dict[str, Any]: """Return engine major periods for one registry key. Raises `DashaNotImplemented` when the key has no engine module. Vimshottari stays on the handler's existing canonical branch and is not computed here. """ if key not in DASHA_REGISTRY_METADATA: raise DashaNotImplemented("Unknown dasha system") if key == "chara": raise DashaNotImplemented("chara dasha is served at /api/dasha/chara") body = body or {} alt, conditional, planet_lons = build_reading_birth_info(birth_dt, planets or {}, ascendant, body) if key == "yogini": payload = _load("yogini_dasha").calculate_yogini_dasha({**alt, "moon_longitude": conditional["moon_longitude"]}) return {"periods": _major_rows(payload)} if key == "ashtottari": payload = _load("ashtottari_dasha").calculate_ashtottari_dasha({ **alt, "moon_longitude": conditional["moon_longitude"], }) return {"periods": _major_rows(payload)} if key == "kalachakra": payload = _load("kalachakra_dasha").calculate_kalachakra_dasha(alt) return {"periods": _major_rows(payload)} if key == "shasti_hayani": birth_info = dict(conditional) birth_info["child_period_profile"] = PYJHORA_PARENT_SEEDED_EQUAL_SPLIT_PROFILE payload = _load("shastihayani_dasha").calculate_shastihayani_dasha(birth_info) return {"periods": _major_rows(payload)} if key in _CONDITIONAL_CALLS: return {"periods": _major_rows(_conditional_payload(key, conditional))} if key == "narayana": asc_idx = int(conditional["ascendant_sign_index"]) reference = _reference_datetime(birth_dt, body) current_age = (reference - birth_dt).days / 365.25 birth_year = int(body.get("year") or birth_dt.year) narayana = _load("narayana_dasha") report = narayana.narayana_dasha_full_report( lagna_sign_idx=asc_idx, planet_lons=planet_lons, current_age=current_age, birth_year=birth_year, ) labeled = _load("narayana_legacy_label").label_legacy_narayana( report, lagna_sign_idx=asc_idx, planet_lons=planet_lons, current_age=current_age, birth_year=birth_year, ) return { "periods": _major_rows(labeled), "algorithm_label": labeled.get("algorithm_label") if isinstance(labeled, dict) else None, "algorithm_note": labeled.get("algorithm_note") if isinstance(labeled, dict) else None, "profile": labeled.get("profile") if isinstance(labeled, dict) else None, } if key == "shoola": rashi = _load("rashi_dashas_native") try: section = rashi.calculate_rashi_dashas( birth_dt.date(), conditional["ascendant_longitude"], planet_lons )["shoola"] except ValueError as exc: raise DashaNotImplemented(str(exc)) from exc return {"periods": _major_rows(section)} raise DashaNotImplemented("dasha system not implemented")