Adds /api/chart_points, a thin route over existing producers (upagrahas, Swiss-Ephemeris special lagnas, Yogi points, arudha padas, friendship), and the chart page's 数据表 (planets, upagrahas, special lagnas, arudhas, shadbala detail, friendship) plus a 标点 layer that marks every point of one group on the board. The ashtakavarga matrix gains the ascendant's own row. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
101 lines
4.2 KiB
Python
101 lines
4.2 KiB
Python
"""`/api/chart_points` (issue #114): the chart page's data tables come from existing producers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SCRIPTS = ROOT / "scripts"
|
|
if str(SCRIPTS) not in sys.path:
|
|
sys.path.insert(0, str(SCRIPTS))
|
|
|
|
from chart_view_points.api import ChartPointsInputError, SIGNS, compute_chart_points # noqa: E402
|
|
|
|
# Real engine /api/chart output (1990-06-15 12:00 Beijing, raman), shared with the frontend tests.
|
|
GOLDEN = json.loads((ROOT / "frontend" / "tests" / "fixtures" / "chart-insight-golden.json").read_text(encoding="utf-8"))
|
|
BODY = {
|
|
"year": 1990, "month": 6, "day": 15, "hour": 12, "minute": 0, "second": 0,
|
|
"lat": 39.9042, "lon": 116.4074, "tz": 8, "ayanamsa": "raman",
|
|
"planets": GOLDEN["chart"]["planets"], "ascendant": GOLDEN["chart"]["ascendant"],
|
|
}
|
|
|
|
|
|
def _sign_index(longitude: float) -> int:
|
|
return int((longitude % 360) // 30)
|
|
|
|
|
|
def test_every_block_is_computed_from_the_engine_producers():
|
|
result = compute_chart_points(BODY)
|
|
assert result["success"] is True
|
|
assert result["upagrahas"]["status"] == "computed"
|
|
assert [row["key"] for row in result["upagrahas"]["rows"]][:5] == ["Dhuma", "Vyatipata", "Parivesha", "Indrachapa", "Upaketu"]
|
|
assert len(result["upagrahas"]["rows"]) == 11
|
|
assert result["special_lagnas"]["status"] == "computed"
|
|
assert len(result["special_lagnas"]["rows"]) == 8
|
|
assert [row["key"] for row in result["yogi_points"]["rows"]] == ["Yogi_Point", "Avayogi_Point"]
|
|
assert [row["key"] for row in result["arudha_padas"]["rows"]] == ["A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", "A11", "UL"]
|
|
assert len(result["friendship"]["rows"]) == 9
|
|
|
|
|
|
def test_points_carry_sign_and_house_counted_from_the_ascendant():
|
|
result = compute_chart_points(BODY)
|
|
asc = _sign_index(float(BODY["ascendant"]["lon"]))
|
|
for block in ("upagrahas", "special_lagnas", "yogi_points"):
|
|
for row in result[block]["rows"]:
|
|
sign = _sign_index(row["longitude"])
|
|
assert row["sign"] == SIGNS[sign]
|
|
assert row["house"] == (sign - asc) % 12 + 1
|
|
assert 0 <= row["degree_in_sign"] < 30
|
|
|
|
|
|
def test_solar_upagrahas_follow_the_sun_formula():
|
|
sun = float(BODY["planets"]["Sun"]["lon"])
|
|
rows = {row["key"]: row["longitude"] for row in compute_chart_points(BODY)["upagrahas"]["rows"]}
|
|
dhuma = (sun + 133 + 20 / 60) % 360
|
|
assert rows["Dhuma"] == pytest.approx(dhuma, abs=1e-3)
|
|
assert rows["Vyatipata"] == pytest.approx((360 - dhuma) % 360, abs=1e-3)
|
|
assert rows["Parivesha"] == pytest.approx((rows["Vyatipata"] + 180) % 360, abs=1e-3)
|
|
|
|
|
|
def test_avayogi_is_yogi_plus_186_40():
|
|
rows = {row["key"]: row["longitude"] for row in compute_chart_points(BODY)["yogi_points"]["rows"]}
|
|
assert (rows["Avayogi_Point"] - rows["Yogi_Point"]) % 360 == pytest.approx(186 + 40 / 60, abs=1e-3)
|
|
|
|
|
|
def test_friendship_compound_is_natural_plus_temporary():
|
|
rows = compute_chart_points(BODY)["friendship"]["rows"]
|
|
sun = next(row for row in rows if row["planet"] == "Sun")
|
|
for target in sun["compound"]["great_friends"]:
|
|
assert target in sun["natural"]["friends"] and target in sun["temporary"]["friends"]
|
|
for target in sun["compound"]["great_enemies"]:
|
|
assert target in sun["natural"]["enemies"] and target in sun["temporary"]["enemies"]
|
|
|
|
|
|
def test_one_failing_block_does_not_fail_the_response(monkeypatch):
|
|
import gulika
|
|
|
|
def broken(*_args, **_kwargs):
|
|
raise RuntimeError("boom")
|
|
|
|
monkeypatch.setattr(gulika, "calculate_upagrahas", broken)
|
|
result = compute_chart_points(BODY)
|
|
assert result["upagrahas"]["status"] == "blocked"
|
|
assert result["arudha_padas"]["status"] == "computed"
|
|
|
|
|
|
def test_missing_birth_or_chart_is_a_bad_request():
|
|
with pytest.raises(ChartPointsInputError):
|
|
compute_chart_points({**BODY, "planets": None})
|
|
with pytest.raises(ChartPointsInputError):
|
|
compute_chart_points({key: value for key, value in BODY.items() if key != "lat"})
|
|
|
|
|
|
def test_route_is_registered_thinly():
|
|
source = (SCRIPTS / "jyotish_api_server.py").read_text(encoding="utf-8")
|
|
assert "elif path == '/api/chart_points':" in source
|
|
assert "_load_local_module('chart_view_points.api')" in source
|