feat(qizheng): vendor stem-branch and expose three read-only chart APIs
Add Apache-2.0 @4n6h4x0r/stem-branch 0.8.0, a seven-governors adapter, and POST /api/qizheng, /api/western, /api/ephemeris_events. BUG-700 remains blocked (do not call --pillars). BUG-701 and BUG-702 are resolved. BUG-703 is investigating (panchanga Lahiri). Skill is not bumped.
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
"""HTTP contracts for POST /api/western and POST /api/ephemeris_events."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from io import BytesIO
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.api_heavy_compute_gate import reset_heavy_compute_gate
|
||||
from scripts.jyotish_api_server import (
|
||||
DEFAULT_ALLOWED_HOSTS,
|
||||
DEFAULT_ALLOWED_ORIGINS,
|
||||
JyotishAPIHandler,
|
||||
)
|
||||
|
||||
SAMPLE = {
|
||||
"year": 1990,
|
||||
"month": 4,
|
||||
"day": 9,
|
||||
"hour": 13,
|
||||
"minute": 24,
|
||||
"lat": 31.19,
|
||||
"lon": 121.44,
|
||||
"tz": 8,
|
||||
}
|
||||
|
||||
|
||||
class _FakeHeaders(dict):
|
||||
def get(self, key, default=None):
|
||||
return super().get(key, default)
|
||||
|
||||
|
||||
class _FakeServer:
|
||||
allowed_origins = DEFAULT_ALLOWED_ORIGINS
|
||||
allowed_hosts = DEFAULT_ALLOWED_HOSTS
|
||||
|
||||
|
||||
class _PostCaptureHandler(JyotishAPIHandler):
|
||||
def __init__(self, path: str, payload: dict) -> None:
|
||||
raw = json.dumps(payload).encode("utf-8")
|
||||
self.headers = _FakeHeaders(
|
||||
{
|
||||
"Content-Length": str(len(raw)),
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
)
|
||||
self.server = _FakeServer()
|
||||
self.path = path
|
||||
self.rfile = BytesIO(raw)
|
||||
self.wfile = BytesIO()
|
||||
self.status_code = None
|
||||
self.response_headers = []
|
||||
self.client_address = ("test-readonly-chart", 0)
|
||||
|
||||
def send_response(self, code, message=None): # noqa: ANN001
|
||||
self.status_code = code
|
||||
|
||||
def send_header(self, key, value): # noqa: ANN001
|
||||
self.response_headers.append((key, value))
|
||||
|
||||
def end_headers(self):
|
||||
return None
|
||||
|
||||
def payload(self) -> dict:
|
||||
return json.loads(self.wfile.getvalue().decode("utf-8"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def api_env(monkeypatch):
|
||||
monkeypatch.setenv("JYOTISH_API_RATE_LIMIT_PER_MINUTE", "0")
|
||||
reset_heavy_compute_gate()
|
||||
yield
|
||||
reset_heavy_compute_gate()
|
||||
|
||||
|
||||
def test_western_returns_planets_cusps_aspects_and_distribution(api_env) -> None:
|
||||
handler = _PostCaptureHandler("/api/western", SAMPLE)
|
||||
handler.do_POST()
|
||||
assert handler.status_code == 200
|
||||
payload = handler.payload()
|
||||
natal = payload["natal"]
|
||||
assert payload["zodiac"] == "tropical"
|
||||
assert payload["house_system"] == "P"
|
||||
assert payload["coordinate_system"] == "tropical_ecliptic"
|
||||
assert len(natal["planets"]) == 11
|
||||
assert len(natal["houses"]) == 12
|
||||
assert natal["aspects"]
|
||||
assert set(natal["distribution"]["elements"]) == {"fire", "earth", "air", "water"}
|
||||
assert "岁差" in payload["boundary"] or "ayanamsa" in json.dumps(payload["sidereal_offset_degrees"])
|
||||
|
||||
|
||||
def test_western_house_system_can_be_overridden_and_echoed(api_env) -> None:
|
||||
handler = _PostCaptureHandler("/api/western", {**SAMPLE, "house_system": "K"})
|
||||
handler.do_POST()
|
||||
assert handler.status_code == 200
|
||||
payload = handler.payload()
|
||||
assert payload["house_system"] == "K"
|
||||
placidus = _PostCaptureHandler("/api/western", SAMPLE)
|
||||
placidus.do_POST()
|
||||
assert placidus.payload()["house_system"] == "P"
|
||||
assert (
|
||||
placidus.payload()["natal"]["houses"][2]["cusp"]["longitude"]
|
||||
!= payload["natal"]["houses"][2]["cusp"]["longitude"]
|
||||
)
|
||||
|
||||
|
||||
def test_ephemeris_events_90_day_window_has_sorted_ingress_and_station(api_env) -> None:
|
||||
handler = _PostCaptureHandler(
|
||||
"/api/ephemeris_events",
|
||||
{
|
||||
"start_date": "2026-09-15",
|
||||
"end_date": "2026-12-14",
|
||||
"ayanamsa": "raman",
|
||||
"node_mode": "mean",
|
||||
},
|
||||
)
|
||||
handler.do_POST()
|
||||
assert handler.status_code == 200
|
||||
payload = handler.payload()
|
||||
events = payload["events"]
|
||||
kinds = {item["kind"] for item in events}
|
||||
assert "ingress" in kinds
|
||||
assert "station" in kinds
|
||||
dates = [item["date"] for item in events]
|
||||
assert dates == sorted(dates)
|
||||
assert payload["calculation"]["ayanamsa"] == "raman"
|
||||
assert len(events) == 14
|
||||
|
||||
|
||||
def test_ephemeris_events_rejects_range_over_730_days(api_env) -> None:
|
||||
handler = _PostCaptureHandler(
|
||||
"/api/ephemeris_events",
|
||||
{"start_date": "2026-01-01", "end_date": "2028-01-02", "ayanamsa": "raman"},
|
||||
)
|
||||
handler.do_POST()
|
||||
assert handler.status_code == 400
|
||||
payload = handler.payload()
|
||||
assert payload["success"] is False
|
||||
assert "730" in payload["error"]
|
||||
|
||||
|
||||
def test_ephemeris_ayanamsa_changes_the_event_list(api_env) -> None:
|
||||
raman = _PostCaptureHandler(
|
||||
"/api/ephemeris_events",
|
||||
{"start_date": "2026-09-15", "end_date": "2026-12-14", "ayanamsa": "raman"},
|
||||
)
|
||||
lahiri = _PostCaptureHandler(
|
||||
"/api/ephemeris_events",
|
||||
{"start_date": "2026-09-15", "end_date": "2026-12-14", "ayanamsa": "lahiri"},
|
||||
)
|
||||
raman.do_POST()
|
||||
lahiri.do_POST()
|
||||
assert raman.status_code == 200
|
||||
assert lahiri.status_code == 200
|
||||
assert raman.payload()["events"] != lahiri.payload()["events"]
|
||||
assert raman.payload()["calculation"]["ayanamsa"] == "raman"
|
||||
assert lahiri.payload()["calculation"]["ayanamsa"] == "lahiri"
|
||||
Reference in New Issue
Block a user