"""PyJHora annual replay boundary. PyJHora/JHora is treated as an external reference engine. This wrapper imports it only inside `replay_pyjhora_annual`, preserves raw return shapes, and never vendors or normalizes it into the native engine as a hard dependency. """ from __future__ import annotations import contextlib import hashlib import json import subprocess import sys from datetime import datetime, timedelta from importlib.metadata import PackageNotFoundError, version as _distribution_version from importlib import import_module as _import_module from inspect import signature from pathlib import Path from typing import Any, Callable ENGINE = "PyJHora/JHora" REPO_ROOT = Path(__file__).resolve().parents[1] _TAJAKA_YEAR_LORD_MODULE = "jhora.horoscope.transit.tajaka" _YEAR_LORD_MODULES = ( "jhora.horoscope.dhasa.annual.mudda", "jhora.horoscope.dhasa.annual.patyayini", ) _YEAR_LORD_CALLABLES = ( "panchadhikari_year_lord", "get_panchadhikari_year_lord", "get_year_lord", "year_lord", ) _ANNUAL_SURFACE_KEYS = ( "annual_chart_snapshot", "annual_chart_speed_snapshot", "mudda_dasha", "patyayini_dasha", "sahams", "solar_return_boundary", "tajika_yogas", ) _PYJHORA_SAHAM_CALLABLES = ( "apamrithyu_saham", "artha_saham", "asha_saham", "bandhana_saham", "bandhu_saham", "bhratri_saham", "gaurava_saham", "jadya_saham", "jalapatna_saham", "jeeva_saham", "kali_saham", "karma_saham", "karyasiddhi_saham", "laabha_saham", "maathri_saham", "mahatmaya_saham", "mitra_saham", "mrithyu_saham", "paradara_saham", "paradesa_saham", "pithri_saham", "preethi_saham", "punya_saham", "puthra_saham", "rajya_saham", "roga_sagam_1", "roga_saham", "samartha_saham", "santapa_saham", "sastra_saham", "sathru_saham", "sraddha_saham", "vanika_saham", "vidya_saham", "vivaha_saham", "vyaapaara_saham", "yasas_saham", ) def probe_pyjhora_year_lord( profile: dict[str, Any], *, birth_julian_day: float, place: Any, age: int | float, import_module: Callable[[str], Any] = _import_module, distribution_version: Callable[[str], str] = _distribution_version, isolated_replay: Callable[[dict[str, Any], float, Any, int | float], dict[str, Any]] | None = None, ) -> dict[str, Any]: """Probe PyJHora for an annual Tajaka/Panchadhikari Year Lord API. This is deliberately an external-engine observation. PyJHora's annual modules are not assumed to implement Panchadhikari simply because they expose other Varshaphala calculations. The public Tajaka callable is preferred when present; annual-dasha modules remain a legacy fallback. """ replay = { "status": "blocked", "reason": "pyjhora_panchadhikari_year_lord_api_missing", "missing_api": "Panchadhikari/Year Lord callable", "probed_modules": [_TAJAKA_YEAR_LORD_MODULE, *_YEAR_LORD_MODULES], "available_annual_callables": [], "raw": None, "normalized_candidates": [], } result = { "engine": ENGINE, "input_profile_id": profile.get("profile_id"), "pyjhora_version": _pyjhora_version(distribution_version), "license_boundary": { "mode": "external_reference_only", "dependency_required": False, }, "evidence_scope": "pyjhora_behavior_only", "parity_status": "not_multiengine_parity", "year_lord_replay": replay, } replay_runner = isolated_replay or _run_isolated_tajaka_year_lord isolated = replay_runner(profile, birth_julian_day, place, age) if isolated.get("reason") != "pyjhora_tajaka_api_missing": replay.update(isolated) replay["normalized_candidates"] = _normalize_tajaka_candidates(replay.get("raw_candidates")) if isolated.get("pyjhora_version"): result["pyjhora_version"] = isolated["pyjhora_version"] return result modules: list[Any] = [] for module_name in _YEAR_LORD_MODULES: try: modules.append(import_module(module_name)) except Exception as exc: replay["status"] = "blocked" replay["reason"] = f"pyjhora_annual_module_import_failed:{module_name}:{exc.__class__.__name__}" replay["missing_api"] = module_name return result for module in modules: replay["available_annual_callables"].extend( sorted(name for name in _YEAR_LORD_CALLABLES if callable(getattr(module, name, None))) ) for name in _YEAR_LORD_CALLABLES: candidate = getattr(module, name, None) if not callable(candidate): continue return _call_year_lord_candidate(result, replay, candidate, name, birth_julian_day, place, age) return result def _run_isolated_tajaka_year_lord( profile: dict[str, Any], birth_julian_day: float, place: Any, age: int | float, ) -> dict[str, Any]: settings = _replay_settings(profile) request = { "birth_julian_day": birth_julian_day, "place": _place_payload(place), "age": age, "settings": settings, } request_hash = _request_hash(request) command = [ sys.executable, "-c", ( "import json, sys; " "from scripts.annual_pyjhora_replay import _isolated_tajaka_worker; " "print(json.dumps(_isolated_tajaka_worker(json.load(sys.stdin)), sort_keys=True))" ), ] try: completed = subprocess.run( command, input=json.dumps(request, sort_keys=True), capture_output=True, text=True, timeout=30, check=False, cwd=str(REPO_ROOT), ) except subprocess.TimeoutExpired: return _isolated_blocked( "pyjhora_isolated_replay_timeout", request_hash, settings ) except Exception as exc: return _isolated_blocked( f"pyjhora_isolated_replay_start_failed:{exc.__class__.__name__}", request_hash, settings ) if completed.returncode != 0: return _isolated_blocked( f"pyjhora_isolated_replay_failed:exit_{completed.returncode}", request_hash, settings ) try: replay = json.loads(completed.stdout) except json.JSONDecodeError: return _isolated_blocked("pyjhora_isolated_replay_invalid_json", request_hash, settings) if not isinstance(replay, dict): return _isolated_blocked("pyjhora_isolated_replay_invalid_payload", request_hash, settings) replay["request_hash"] = request_hash return replay def _isolated_tajaka_worker(request: dict[str, Any]) -> dict[str, Any]: """Run in a child process so PyJHora global Ayanamsa state cannot leak.""" settings = dict(request.get("settings") or {}) node_mode = { "requested": settings.get("node_mode"), "status": "unsupported_by_replay_adapter", } ayanamsa = settings.get("ayanamsa") if not isinstance(ayanamsa, str) or not ayanamsa.strip(): return { "status": "blocked", "reason": "pyjhora_ayanamsa_configuration_failed:missing_ayanamsa", "settings": settings, "node_mode": node_mode, } try: # PyJHora can print import diagnostics; reserve stdout for the JSON reply. with contextlib.redirect_stdout(sys.stderr): from jhora import const from jhora.panchanga import drik from jhora.horoscope.transit import tajaka drik.set_ayanamsa_mode(ayanamsa.upper()) effective_ayanamsa = const._DEFAULT_AYANAMSA_MODE if str(effective_ayanamsa).upper() != ayanamsa.upper(): return { "status": "blocked", "reason": "pyjhora_ayanamsa_configuration_failed:effective_mode_mismatch", "settings": settings, "effective_ayanamsa": effective_ayanamsa, "node_mode": node_mode, } except Exception as exc: return { "status": "blocked", "reason": f"pyjhora_ayanamsa_configuration_failed:{exc.__class__.__name__}", "settings": settings, "node_mode": node_mode, } try: place_data = dict(request["place"]) place = drik.Place( place_data["name"], float(place_data["latitude"]), float(place_data["longitude"]), float(place_data["timezone"]), place_data.get("elevation"), ) raw_candidates, candidate_trace_status = _tajaka_candidate_trace( tajaka, float(request["birth_julian_day"]), place, request["age"] ) raw = tajaka.lord_of_the_year(float(request["birth_julian_day"]), place, request["age"]) except Exception as exc: return { "status": "blocked", "reason": f"pyjhora_tajaka_year_lord_failed:{exc.__class__.__name__}", "callable": f"{_TAJAKA_YEAR_LORD_MODULE}.lord_of_the_year", "settings": settings, "effective_ayanamsa": effective_ayanamsa, "node_mode": node_mode, "raw": None, "raw_candidates": None, } return { "status": "partial_verified", "reason": "pyjhora_tajaka_year_lord_callable_observed", "callable": f"{_TAJAKA_YEAR_LORD_MODULE}.lord_of_the_year", "pyjhora_version": _pyjhora_version(_distribution_version), "settings": settings, "effective_ayanamsa": effective_ayanamsa, "node_mode": node_mode, "year_lord": raw, "raw": raw, "raw_candidates": raw_candidates, "candidate_trace_status": candidate_trace_status, } def _run_isolated_annual_replay( profile: dict[str, Any], birth_julian_day: float, place: Any, age: int | float, ) -> dict[str, Any]: """Call optional PyJHora annual surfaces with a child-process Place.""" settings = _replay_settings(profile) request = { "birth_julian_day": birth_julian_day, "place": _place_payload(place), "age": age, "settings": settings, } request_hash = _request_hash(request) command = [ sys.executable, "-c", ( "import json, sys; " "from scripts.annual_pyjhora_replay import _isolated_annual_worker; " "print(json.dumps(_isolated_annual_worker(json.load(sys.stdin)), sort_keys=True, default=str))" ), ] try: completed = subprocess.run( command, input=json.dumps(request, sort_keys=True), capture_output=True, text=True, timeout=30, check=False, cwd=str(REPO_ROOT), ) except subprocess.TimeoutExpired: return _annual_replay_blocked("pyjhora_isolated_annual_replay_timeout", request_hash, settings) except Exception as exc: return _annual_replay_blocked( f"pyjhora_isolated_annual_replay_start_failed:{exc.__class__.__name__}", request_hash, settings ) if completed.returncode != 0: return _annual_replay_blocked( f"pyjhora_isolated_annual_replay_failed:exit_{completed.returncode}", request_hash, settings ) try: replay = json.loads(completed.stdout) except json.JSONDecodeError: return _annual_replay_blocked("pyjhora_isolated_annual_replay_invalid_json", request_hash, settings) if not isinstance(replay, dict): return _annual_replay_blocked("pyjhora_isolated_annual_replay_invalid_payload", request_hash, settings) replay["request_hash"] = request_hash return replay def _isolated_annual_worker(request: dict[str, Any]) -> dict[str, Any]: """Construct ``drik.Place`` after setting Ayanamsa in the child process.""" settings = dict(request.get("settings") or {}) node_mode = {"requested": settings.get("node_mode"), "status": "unsupported_by_replay_adapter"} ayanamsa = settings.get("ayanamsa") if not isinstance(ayanamsa, str) or not ayanamsa.strip(): return _annual_worker_result( {key: {"status": "blocked", "reason": "pyjhora_ayanamsa_configuration_failed:missing_ayanamsa"} for key in _ANNUAL_SURFACE_KEYS}, settings=settings, node_mode=node_mode, ) try: with contextlib.redirect_stdout(sys.stderr): from jhora import const from jhora.panchanga import drik from jhora.horoscope.dhasa.annual import mudda, patyayini from jhora.horoscope.transit import saham drik.set_ayanamsa_mode(ayanamsa.upper()) effective_ayanamsa = const._DEFAULT_AYANAMSA_MODE if str(effective_ayanamsa).upper() != ayanamsa.upper(): return _annual_worker_result( {key: {"status": "blocked", "reason": "pyjhora_ayanamsa_configuration_failed:effective_mode_mismatch"} for key in _ANNUAL_SURFACE_KEYS}, settings=settings, node_mode=node_mode, effective_ayanamsa=effective_ayanamsa, ) place_data = dict(request["place"]) pyjhora_place = drik.Place( place_data["name"], float(place_data["latitude"]), float(place_data["longitude"]), float(place_data["timezone"]), place_data.get("elevation"), ) except Exception as exc: return _annual_worker_result( {key: {"status": "blocked", "reason": f"pyjhora_annual_adapter_setup_failed:{exc.__class__.__name__}"} for key in _ANNUAL_SURFACE_KEYS}, settings=settings, node_mode=node_mode, ) jd = float(request["birth_julian_day"]) years = request["age"] surfaces = { "annual_chart_snapshot": _call_annual_chart_snapshot(jd, pyjhora_place, years), "annual_chart_speed_snapshot": _call_annual_chart_speed_snapshot(jd, pyjhora_place, years), "mudda_dasha": _call_mudda(mudda, jd, pyjhora_place, years), "patyayini_dasha": _call_patyayini(patyayini, jd, pyjhora_place, years), "sahams": _call_sahams(saham, jd, pyjhora_place, years), "solar_return_boundary": _call_solar_return_boundary(saham, jd, pyjhora_place, years), "tajika_yogas": _probe_tajika_yogas(jd, pyjhora_place, years), } return _annual_worker_result( surfaces, settings=settings, node_mode=node_mode, effective_ayanamsa=effective_ayanamsa, ) def _annual_worker_result( surfaces: dict[str, dict[str, Any]], *, settings: dict[str, Any], node_mode: dict[str, Any], effective_ayanamsa: Any = None, ) -> dict[str, Any]: return { **{key: surfaces.get(key, {"status": "blocked", "reason": "not_executed"}) for key in _ANNUAL_SURFACE_KEYS}, "settings": settings, "effective_ayanamsa": effective_ayanamsa, "node_mode": node_mode, "evidence_scope": "pyjhora_behavior_only", "parity_status": "not_multiengine_parity", } def _annual_replay_blocked(reason: str, request_hash: str, settings: dict[str, Any]) -> dict[str, Any]: return _annual_worker_result( {key: {"status": "blocked", "reason": reason} for key in _ANNUAL_SURFACE_KEYS}, settings=settings, node_mode={"requested": settings.get("node_mode"), "status": "unsupported_by_replay_adapter"}, ) | {"request_hash": request_hash} def _place_payload(place: Any) -> dict[str, Any]: return { "name": getattr(place, "name", "External replay place"), "latitude": getattr(place, "latitude"), "longitude": getattr(place, "longitude"), "timezone": getattr(place, "timezone"), "elevation": getattr(place, "elevation", None), } def _request_hash(request: dict[str, Any]) -> str: payload = json.dumps(request, sort_keys=True, separators=(",", ":"), ensure_ascii=True) return hashlib.sha256(payload.encode("utf-8")).hexdigest() def _isolated_blocked(reason: str, request_hash: str, settings: dict[str, Any]) -> dict[str, Any]: return { "status": "blocked", "reason": reason, "request_hash": request_hash, "settings": settings, "node_mode": { "requested": settings.get("node_mode"), "status": "unsupported_by_replay_adapter", }, } def _replay_settings(profile: dict[str, Any]) -> dict[str, Any]: settings = profile.get("settings") source = settings if isinstance(settings, dict) else profile return { key: source[key] for key in ("ayanamsa", "node_mode") if source.get(key) is not None } def _call_tajaka_year_lord( result: dict[str, Any], replay: dict[str, Any], tajaka: Any, *, birth_julian_day: float, place: Any, age: int | float, settings: dict[str, Any], ) -> dict[str, Any]: callable_name = f"{_TAJAKA_YEAR_LORD_MODULE}.lord_of_the_year" try: raw_candidates, candidate_trace_status = _tajaka_candidate_trace( tajaka, birth_julian_day, place, age ) raw = tajaka.lord_of_the_year(birth_julian_day, place, age) except Exception as exc: replay.update( { "status": "blocked", "reason": f"pyjhora_tajaka_year_lord_failed:{exc.__class__.__name__}", "missing_api": None, "callable": callable_name, "settings": settings, "raw": None, "raw_candidates": None, } ) return result replay.update( { "status": "partial_verified", "reason": "pyjhora_tajaka_year_lord_callable_observed", "missing_api": None, "callable": callable_name, "settings": settings, "year_lord": raw, "raw": raw, "raw_candidates": raw_candidates, "candidate_trace_status": candidate_trace_status, "normalized_candidates": _normalize_tajaka_candidates(raw_candidates), } ) return result def _tajaka_candidate_trace( tajaka: Any, birth_julian_day: float, place: Any, age: int | float, ) -> tuple[Any, str]: """Collect the source module's candidate list without reimplementing its rule.""" candidate_function = getattr(tajaka, "_get_lord_candidates", None) if not callable(candidate_function): return None, "unavailable" try: # Lightweight injected test modules can expose a direct trace helper. return candidate_function(), "observed" except TypeError: pass try: rasi_chart = tajaka.charts.divisional_chart( birth_julian_day, place, divisional_chart_factor=1 ) natal_planet_to_house = tajaka.utils.get_planet_house_dictionary_from_planet_positions(rasi_chart) natal_lagna_house = natal_planet_to_house[tajaka.const._ascendant_symbol] annual_jd = birth_julian_day + float(age) * tajaka.year_value annual_hour = tajaka.drik.jd_to_gregorian(annual_jd)[3] sunrise = tajaka.utils.from_dms_str_to_dms(tajaka.drik.sunrise(annual_jd, place)[1]) sunset = tajaka.utils.from_dms_str_to_dms(tajaka.drik.sunset(annual_jd, place)[1]) sunrise_hour = sunrise[0] + sunrise[1] / 60 + sunrise[2] / 3600 sunset_hour = sunset[0] + sunset[1] / 60 + sunset[2] / 3600 night_time_birth = annual_hour > sunset_hour or annual_hour < sunrise_hour annual_chart = tajaka.charts.divisional_chart(annual_jd, place, divisional_chart_factor=1) return candidate_function(annual_chart, age, natal_lagna_house, night_time_birth), "observed" except Exception as exc: return None, f"blocked:{exc.__class__.__name__}" def _normalize_tajaka_candidates(raw_candidates: Any) -> list[dict[str, Any]]: if not isinstance(raw_candidates, (list, tuple)): return [] return [ {"planet_index": candidate, "selection_state": "raw_tajaka_candidate"} for candidate in raw_candidates if isinstance(candidate, int) ] def _pyjhora_version(distribution_version: Callable[[str], str]) -> str | None: try: return distribution_version("PyJHora") except PackageNotFoundError: return None def _call_year_lord_candidate( result: dict[str, Any], replay: dict[str, Any], candidate: Callable[..., Any], callable_name: str, birth_julian_day: float, place: Any, age: int | float, ) -> dict[str, Any]: values = { "jd": birth_julian_day, "birth_julian_day": birth_julian_day, "place": place, "age": age, "years": age, } try: params = signature(candidate).parameters kwargs = {name: values[name] for name in params if name in values} required = [ name for name, parameter in params.items() if parameter.default is parameter.empty and parameter.kind in (parameter.POSITIONAL_ONLY, parameter.POSITIONAL_OR_KEYWORD, parameter.KEYWORD_ONLY) and name not in kwargs ] if required: replay.update( { "reason": "pyjhora_year_lord_callable_signature_unsupported", "missing_api": ", ".join(required), "callable": callable_name, } ) return result raw = candidate(**kwargs) except Exception as exc: replay.update( { "reason": f"pyjhora_year_lord_callable_failed:{callable_name}:{exc.__class__.__name__}", "missing_api": None, "callable": callable_name, } ) return result replay.update( { "status": "partial_verified", "reason": "pyjhora_year_lord_callable_observed", "missing_api": None, "callable": callable_name, "year_lord": raw.get("year_lord") if isinstance(raw, dict) else None, "raw": raw, "normalized_candidates": _normalize_year_lord_candidates(raw), } ) return result def _normalize_year_lord_candidates(raw: Any) -> list[dict[str, Any]]: if not isinstance(raw, dict): return [] candidates = raw.get("candidates") or raw.get("candidate_planets") if not isinstance(candidates, list): return [] return [candidate for candidate in candidates if isinstance(candidate, dict)] def replay_pyjhora_annual( profile: dict[str, Any], *, birth_julian_day: float, place: Any, age: int | float, import_module: Callable[[str], Any] = _import_module, year_lord_probe: Callable[..., dict[str, Any]] | None = None, isolated_annual_replay: Callable[[dict[str, Any], float, Any, int | float], dict[str, Any]] | None = None, ) -> dict[str, Any]: """Replay annual PyJHora methods for the same profile, if available.""" year_lord_observer = year_lord_probe or probe_pyjhora_year_lord year_lord_result = year_lord_observer( profile, birth_julian_day=birth_julian_day, place=place, age=age, ) year_lord_replay = year_lord_result.get("year_lord_replay") if isinstance(year_lord_result, dict) else None blocked = _base(profile) blocked["year_lord_replay"] = year_lord_replay if isinstance(year_lord_replay, dict) else { "status": "blocked", "reason": "pyjhora_year_lord_probe_invalid_payload", } blocked["evidence_scope"] = "pyjhora_behavior_only" blocked["parity_status"] = "not_multiengine_parity" if isolated_annual_replay is not None or (import_module is _import_module and year_lord_probe is None): observer = isolated_annual_replay or _run_isolated_annual_replay observed = observer(profile, birth_julian_day, place, age) if not isinstance(observed, dict): observed = _annual_replay_blocked( "pyjhora_isolated_annual_replay_invalid_payload", _request_hash({"birth_julian_day": birth_julian_day, "place": _place_payload(place), "age": age}), _replay_settings(profile), ) result = _base(profile) for key in _ANNUAL_SURFACE_KEYS: value = observed.get(key) result[key] = value if isinstance(value, dict) else {"status": "blocked", "reason": f"{key}_missing_from_isolated_replay"} result["year_lord_replay"] = blocked["year_lord_replay"] for key in ("effective_ayanamsa", "node_mode", "request_hash"): if key in observed: result[key] = observed[key] result["evidence_scope"] = "pyjhora_behavior_only" result["parity_status"] = "not_multiengine_parity" result["status"] = _aggregate_status(result) return result try: mudda = import_module("jhora.horoscope.dhasa.annual.mudda") patyayini = import_module("jhora.horoscope.dhasa.annual.patyayini") saham = import_module("jhora.horoscope.transit.saham") except Exception as exc: reason = f"pyjhora_import_failed:{exc.__class__.__name__}:{exc}" for key in _ANNUAL_SURFACE_KEYS: blocked[key] = {"status": "blocked", "reason": reason} blocked["status"] = "blocked" return blocked result = _base(profile) result["annual_chart_snapshot"] = _call_annual_chart_snapshot(birth_julian_day, place, age) result["mudda_dasha"] = _call_mudda(mudda, birth_julian_day, place, age) result["patyayini_dasha"] = _call_patyayini(patyayini, birth_julian_day, place, age) result["sahams"] = _call_sahams(saham, birth_julian_day, place, age) result["solar_return_boundary"] = _call_solar_return_boundary(saham, birth_julian_day, place, age) result["tajika_yogas"] = {"status": "blocked", "reason": "pyjhora_tajika_yogas_requires_isolated_replay"} result["year_lord_replay"] = year_lord_replay if isinstance(year_lord_replay, dict) else { "status": "blocked", "reason": "pyjhora_year_lord_probe_invalid_payload", } result["evidence_scope"] = "pyjhora_behavior_only" result["parity_status"] = "not_multiengine_parity" result["status"] = _aggregate_status(result) return result def _base(profile: dict[str, Any]) -> dict[str, Any]: return { "engine": ENGINE, "license_boundary": { "mode": "external_reference_only", "dependency_required": False, "notes": "PyJHora/JHora remains an optional external oracle/replay boundary.", }, "input_profile_id": profile.get("profile_id"), "annual_chart_snapshot": {"status": "blocked", "reason": "not_executed"}, "annual_chart_speed_snapshot": {"status": "blocked", "reason": "not_executed"}, "mudda_dasha": {"status": "blocked", "reason": "not_executed"}, "patyayini_dasha": {"status": "blocked", "reason": "not_executed"}, "sahams": {"status": "blocked", "reason": "not_executed"}, "solar_return_boundary": {"status": "blocked", "reason": "not_executed"}, "tajika_yogas": {"status": "blocked", "reason": "not_executed"}, "year_lord_replay": {"status": "blocked", "reason": "not_executed"}, "evidence_scope": "pyjhora_behavior_only", "parity_status": "not_multiengine_parity", "status": "blocked", } def _call_mudda(module: Any, birth_julian_day: float, place: Any, age: int | float) -> dict[str, Any]: payload: dict[str, Any] = {} try: if hasattr(module, "varsha_vimsottari_dasha_start_date"): payload["varsha_vimsottari_start"] = module.varsha_vimsottari_dasha_start_date( birth_julian_day, place, age ) if hasattr(module, "mudda_dhasa_bhukthi"): payload["periods"] = module.mudda_dhasa_bhukthi(birth_julian_day, place, age) return {"status": "partial_verified", "raw": payload} except Exception as exc: return {"status": "blocked", "reason": f"pyjhora_mudda_failed:{exc}"} def _call_patyayini(module: Any, birth_julian_day: float, place: Any, age: int | float) -> dict[str, Any]: try: jd_for_year = birth_julian_day + float(age) * 365.256364 return {"status": "partial_verified", "raw": {"periods": module.get_dhasa_bhukthi(jd_for_year, place)}} except Exception as exc: return {"status": "blocked", "reason": f"pyjhora_patyayini_failed:{exc}"} def _call_sahams(module: Any, birth_julian_day: float, place: Any, age: int | float) -> dict[str, Any]: """Replay the current PyJHora per-Saham API against its annual chart. Older PyJHora releases exposed an aggregate ``sahams`` function. Current releases expose individual functions which all consume annual-chart planet positions. Keep both paths external-reference-only and preserve the exact source API used for audit. """ try: individual = [name for name in _PYJHORA_SAHAM_CALLABLES if callable(getattr(module, name, None))] if not individual: if hasattr(module, "sahams"): return { "status": "partial_verified", "callable": "jhora.horoscope.transit.saham.sahams", "raw": module.sahams(birth_julian_day, place, age), "reason": "legacy_pyjhora_aggregate_sahams_api", } return {"status": "blocked", "reason": "pyjhora_sahams_callable_missing"} tajaka = _import_module("jhora.horoscope.transit.tajaka") annual_chart = getattr(tajaka, "annual_chart", None) if not callable(annual_chart): return {"status": "blocked", "reason": "pyjhora_annual_chart_callable_missing"} chart_payload = annual_chart(birth_julian_day, place, years=age) if not isinstance(chart_payload, tuple) or len(chart_payload) < 2 or not isinstance(chart_payload[0], list): return {"status": "blocked", "reason": "pyjhora_annual_chart_payload_invalid"} positions, return_marker = chart_payload[0], chart_payload[1] annual_moment = _annual_return_datetime(return_marker) if annual_moment is None: return {"status": "blocked", "reason": "pyjhora_annual_chart_return_time_unavailable"} try: try: from scripts.saham_daynight import determine_daytime except ImportError: # pragma: no cover - direct script execution from saham_daynight import determine_daytime daynight = determine_daytime( annual_moment, lat=float(place.latitude), lon=float(place.longitude), tz=float(place.timezone), ) except Exception as exc: return {"status": "blocked", "reason": f"pyjhora_sahams_daynight_failed:{exc.__class__.__name__}"} values: dict[str, Any] = {} failures: dict[str, str] = {} for name in individual: candidate = getattr(module, name) try: parameters = signature(candidate).parameters if "night_time_birth" in parameters: values[name] = candidate(positions, night_time_birth=not bool(daynight["is_daytime"])) else: values[name] = candidate(positions) except Exception as exc: # External API variations must remain visible. failures[name] = exc.__class__.__name__ if not values: return { "status": "blocked", "reason": "pyjhora_individual_sahams_all_failed", "failed_callables": failures, } return { "status": "partial_verified", "callable": "jhora.horoscope.transit.tajaka.annual_chart", "reason": "pyjhora_individual_sahams_from_annual_chart", "daynight": daynight, "raw": { "annual_return": _annual_return_marker(return_marker), "sahams": values, "failed_callables": failures, "available_callables": individual, }, } except Exception as exc: return {"status": "blocked", "reason": f"pyjhora_sahams_failed:{exc}"} def _call_annual_chart_snapshot(birth_julian_day: float, place: Any, age: int | float) -> dict[str, Any]: """Capture PyJHora's annual chart without deriving a local interpretation. This is an external-reference snapshot for profile diagnosis. It exists so annual Tajika comparisons can distinguish a chart-input difference from a candidate-rule difference. It is not a report authority or a Yoga result. """ try: tajaka = _import_module("jhora.horoscope.transit.tajaka") annual_chart = getattr(tajaka, "annual_chart", None) if not callable(annual_chart): return {"status": "blocked", "reason": "pyjhora_annual_chart_callable_missing"} payload = annual_chart(birth_julian_day, place, years=age) if not isinstance(payload, tuple) or len(payload) < 2 or not isinstance(payload[0], list): return {"status": "blocked", "reason": "pyjhora_annual_chart_payload_invalid"} positions, return_marker = payload[0], payload[1] return { "status": "partial_verified", "callable": "jhora.horoscope.transit.tajaka.annual_chart", "reason": "pyjhora_annual_chart_snapshot_for_profile_comparison", "raw": { "annual_return": _annual_return_marker(return_marker), "positions": positions, }, "claim_boundary": "external_annual_chart_snapshot_is_not_tajika_yoga_or_report_authority", } except Exception as exc: return {"status": "blocked", "reason": f"pyjhora_annual_chart_snapshot_failed:{exc.__class__.__name__}"} def _signed_longitude_delta(previous: float, following: float) -> float: return (float(following) - float(previous) + 180.0) % 360.0 - 180.0 def _planet_longitudes_from_positions(positions: Any) -> dict[int, float]: values: dict[int, float] = {} for row in positions if isinstance(positions, list) else []: if not isinstance(row, (list, tuple)) or len(row) < 2 or not isinstance(row[0], int): continue planet, position = row[0], row[1] if planet not in range(7) or not isinstance(position, (list, tuple)) or len(position) < 2: continue sign, degree = position[0], position[1] if isinstance(sign, (int, float)) and isinstance(degree, (int, float)): values[planet] = (float(sign) * 30.0 + float(degree)) % 360.0 return values def _call_annual_chart_speed_snapshot(birth_julian_day: float, place: Any, age: int | float) -> dict[str, Any]: """Observe PyJHora annual-chart motion by finite difference. The upstream annual-chart snapshot exposes longitudes but not speeds. This child-process-only probe samples one minute on either side of the external return marker so a later diagnostic can distinguish chart coordinates from applying/separating semantics. It is not a native speed source or Tajika Yoga authority. """ try: from jhora import utils from jhora.panchanga import drik tajaka = _import_module("jhora.horoscope.transit.tajaka") annual_chart = getattr(tajaka, "annual_chart", None) if not callable(annual_chart): return {"status": "blocked", "reason": "pyjhora_annual_chart_callable_missing"} payload = annual_chart(birth_julian_day, place, years=age) if not isinstance(payload, tuple) or len(payload) < 2: return {"status": "blocked", "reason": "pyjhora_annual_chart_payload_invalid"} annual_moment = _annual_return_datetime(payload[1]) if annual_moment is None: return {"status": "blocked", "reason": "pyjhora_annual_chart_return_time_unavailable"} offset_seconds = 60 before = annual_moment - timedelta(seconds=offset_seconds) after = annual_moment + timedelta(seconds=offset_seconds) before_jd = utils.julian_day_number( (before.year, before.month, before.day), (before.hour, before.minute, before.second) ) after_jd = utils.julian_day_number( (after.year, after.month, after.day), (after.hour, after.minute, after.second) ) before_positions = _planet_longitudes_from_positions(drik.dhasavarga(before_jd, place)) after_positions = _planet_longitudes_from_positions(drik.dhasavarga(after_jd, place)) if set(before_positions) != set(range(7)) or set(after_positions) != set(range(7)): return { "status": "blocked", "reason": "pyjhora_annual_chart_speed_positions_incomplete", "before_planets": sorted(before_positions), "after_planets": sorted(after_positions), } interval_days = (2.0 * offset_seconds) / 86400.0 rates = { planet: round(_signed_longitude_delta(before_positions[planet], after_positions[planet]) / interval_days, 8) for planet in range(7) } return { "status": "partial_verified", "callable": "jhora.panchanga.drik.dhasavarga", "reason": "pyjhora_annual_chart_finite_difference_speed_snapshot", "raw": { "annual_return": _annual_return_marker(payload[1]), "sample_offset_seconds": offset_seconds, "planet_rates_deg_per_day": rates, }, "claim_boundary": "external_annual_speed_snapshot_is_comparison_only_not_tajika_yoga_or_report_authority", } except Exception as exc: return {"status": "blocked", "reason": f"pyjhora_annual_chart_speed_snapshot_failed:{exc.__class__.__name__}"} def _annual_return_marker(value: Any) -> dict[str, Any]: if not isinstance(value, (list, tuple)) or len(value) < 2: return {"raw": value} date_value, time_value = value[0], value[1] return { "date": list(date_value) if isinstance(date_value, (list, tuple)) else date_value, "time": str(time_value), } def _annual_return_datetime(value: Any) -> datetime | None: marker = _annual_return_marker(value) date_value = marker.get("date") time_value = marker.get("time") if not isinstance(date_value, list) or len(date_value) != 3 or not isinstance(time_value, str): return None try: hour, minute, second = [int(part) for part in time_value.split(":")] return datetime(int(date_value[0]), int(date_value[1]), int(date_value[2]), hour, minute, second) except (TypeError, ValueError): return None def _call_solar_return_boundary(module: Any, birth_julian_day: float, place: Any, age: int | float) -> dict[str, Any]: try: if not hasattr(module, "solar_return_chart"): return {"status": "blocked", "reason": "pyjhora_solar_return_chart_missing"} return { "status": "partial_verified", "raw": module.solar_return_chart(birth_julian_day, place, age), } except Exception as exc: return {"status": "blocked", "reason": f"pyjhora_solar_return_failed:{exc}"} def _probe_tajika_yogas(birth_julian_day: float, place: Any, age: int | float) -> dict[str, Any]: """Probe callable availability without recreating PyJHora yoga rules.""" try: module = _import_module("jhora.horoscope.transit.tajaka_yoga") except Exception as exc: return {"status": "blocked", "reason": f"pyjhora_tajika_yogas_module_missing:{exc.__class__.__name__}"} for name in ("tajaka_yoga", "tajika_yoga", "get_tajaka_yogas", "get_tajika_yogas"): candidate = getattr(module, name, None) if not callable(candidate): continue values = {"jd": birth_julian_day, "birth_julian_day": birth_julian_day, "place": place, "age": age, "years": age} try: params = signature(candidate).parameters kwargs = {key: values[key] for key in params if key in values} missing = [key for key, parameter in params.items() if parameter.default is parameter.empty and key not in kwargs] if missing: return {"status": "blocked", "reason": "pyjhora_tajika_yogas_callable_signature_unsupported", "callable": name, "missing": missing} return {"status": "partial_verified", "callable": name, "raw": candidate(**kwargs)} except Exception as exc: return {"status": "blocked", "reason": f"pyjhora_tajika_yogas_callable_failed:{exc.__class__.__name__}", "callable": name} return {"status": "blocked", "reason": "pyjhora_tajika_yogas_callable_missing"} def _aggregate_status(result: dict[str, Any]) -> str: statuses = { result[key].get("status") for key in (*_ANNUAL_SURFACE_KEYS, "year_lord_replay") if isinstance(result.get(key), dict) } if statuses == {"partial_verified"}: return "partial_verified" if "partial_verified" in statuses: return "partial_verified" return "blocked"