_compute_shadbala called swe.julday with no swe in scope, so any request with year/month/day/lat/lon raised NameError. The endpoint now lives in scripts/shadbala_service/api.py, which imports Swiss Ephemeris itself; the handler only forwards. _shadbala_rule_variants and _compute_shadbala_advanced_layer moved with it (handler methods 217 -> 215, baseline lowered). New real-HTTP regression test with ruff F821 on the server, added to the quick gate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DbBXoyeZrYn6ryNcSe5Y2Y
155 lines
7.1 KiB
Python
155 lines
7.1 KiB
Python
"""`/api/shadbala`: the six-fold strength core plus the advanced evidence layer.
|
|
|
|
Moved out of ``JyotishAPIHandler`` for BUG-1463. The handler method used
|
|
``swe.julday`` with no ``swe`` in scope, so every request carrying
|
|
year/month/day/lat/lon failed with ``NameError`` (HTTP 500). This module
|
|
imports Swiss Ephemeris itself; the handler only forwards the body.
|
|
|
|
The handler is passed in for its existing body parsers (``_get_float``,
|
|
``_parse_timezone`` ...). They raise the server's ``BadRequest`` and that
|
|
propagates unchanged. This module never imports the HTTP server.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
import swisseph as swe
|
|
|
|
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME, UnsupportedAyanamsaError, normalize_ayanamsa_name
|
|
|
|
SIGNS = (
|
|
"Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo",
|
|
"Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces",
|
|
)
|
|
BIRTH_KEYS = ("year", "month", "day", "lat", "lon")
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ShadbalaInputError(ValueError):
|
|
"""The request names an ayanamsa the engine does not support."""
|
|
|
|
|
|
def _ayanamsa(body: dict[str, Any]) -> str:
|
|
"""Same reading as the server's ``_request_ayanamsa(body)``."""
|
|
raw = body.get("ayanamsa") or body.get("ayanamsa_name") or body.get("ayanamsa_policy")
|
|
if raw in (None, ""):
|
|
return DEFAULT_AYANAMSA_NAME
|
|
try:
|
|
return normalize_ayanamsa_name(raw)
|
|
except UnsupportedAyanamsaError as exc:
|
|
raise ShadbalaInputError(str(exc)) from exc
|
|
|
|
|
|
def birth_context(handler: Any, body: dict[str, Any], birth_hour: float, birth_minute: float, birth_second: float) -> dict[str, Any] | None:
|
|
"""Precise Swiss Ephemeris context when the body carries the full birth moment."""
|
|
if not all(key in body for key in BIRTH_KEYS):
|
|
return None
|
|
import shadbala
|
|
|
|
year = handler._get_int(body, "year", 1990, 1800, 2400)
|
|
month = handler._get_int(body, "month", 6, 1, 12)
|
|
day = handler._get_int(body, "day", 15, 1, 31)
|
|
lat = handler._get_float(body, "lat", 0, -90, 90)
|
|
lon = handler._get_float(body, "lon", 0, -180, 180)
|
|
tz = handler._parse_timezone(body, lat, lon, year, month, day, birth_hour, birth_minute, birth_second)
|
|
hour_decimal = handler._birth_hour_decimal(birth_hour, birth_minute, birth_second)
|
|
jd_ut = swe.julday(year, month, day, hour_decimal - tz)
|
|
return shadbala.build_shadbala_context(jd_ut, lat, lon, _ayanamsa(body), tz)
|
|
|
|
|
|
def rule_variants(advanced_enabled: bool) -> dict[str, Any]:
|
|
return {
|
|
"selected": ["core_sixfold", "advanced_evidence"] if advanced_enabled else ["core_sixfold"],
|
|
"available": [
|
|
{"key": "core_sixfold", "label": "六重力量主算法", "status": "active", "source": "scripts/shadbala.py"},
|
|
{"key": "advanced_evidence", "label": "Kala/Yuddha/Sputa 增强证据", "status": "active" if advanced_enabled else "unavailable", "source": "scripts/shadbala_advanced.py"},
|
|
],
|
|
"boundary": "增强层只作为证据补充,不覆盖主 Shadbala 的总分与排名。",
|
|
}
|
|
|
|
|
|
def advanced_layer(handler: Any, body: dict[str, Any], planets: dict[str, Any], base_result: Any) -> dict[str, Any]:
|
|
"""Kala sub-items, Yuddha and Sputa Drishti evidence; ``{}`` when it cannot be built."""
|
|
from datetime import datetime
|
|
|
|
try:
|
|
import shadbala_advanced as advanced_mod
|
|
|
|
birth_hour = handler._get_float(body, "birth_hour", body.get("hour", 12), 0, 23)
|
|
birth_minute = handler._get_float(body, "birth_minute", body.get("minute", 0), 0, 59)
|
|
birth_second = handler._get_birth_second(body)
|
|
year = handler._get_int(body, "year", datetime.now().year, 1800, 2400)
|
|
month = handler._get_int(body, "month", 1, 1, 12)
|
|
day = handler._get_int(body, "day", 1, 1, 31)
|
|
lat = handler._get_float(body, "lat", body.get("birth_lat", 0), -90, 90)
|
|
lon = handler._get_float(body, "lon", body.get("birth_lon", 0), -180, 180)
|
|
tz = handler._parse_timezone(body, lat, lon, year, month, day, birth_hour, birth_minute, birth_second)
|
|
hour_decimal = handler._birth_hour_decimal(birth_hour, birth_minute, birth_second)
|
|
solar_lon = planets.get("Sun", {}).get("lon", 0)
|
|
base_planets = base_result.get("planets", {}) if isinstance(base_result, dict) else {}
|
|
comparison_planets = {}
|
|
kala_additions = {}
|
|
drik_sputa = {}
|
|
for planet, pdata in planets.items():
|
|
if planet not in base_planets:
|
|
continue
|
|
comparison_planets[planet] = {
|
|
**pdata,
|
|
"degree": pdata.get("lon", pdata.get("degree", 0)),
|
|
"shadbala_total": base_planets.get(planet, {}).get("total_virupas", 0),
|
|
}
|
|
for planet in comparison_planets:
|
|
kala_additions[planet] = round(advanced_mod.calc_varsha_maasa_dina_hora_bala(
|
|
planet, year, month, day, hour_decimal, solar_lon, lat, lon, tz
|
|
), 2)
|
|
drik_sputa[planet] = round(advanced_mod.calc_drik_bala_sputa(planet, comparison_planets), 2)
|
|
yuddha = advanced_mod.calc_yuddha_bala(comparison_planets)
|
|
active_yuddha = {planet: value for planet, value in yuddha.items() if abs(value) > 0}
|
|
top_kala = sorted(kala_additions.items(), key=lambda item: item[1], reverse=True)[:3]
|
|
return {
|
|
"source": "scripts/shadbala_advanced.py",
|
|
"method": "Kala Bala完整子项 + Yuddha Bala + Sputa Drishti 证据层",
|
|
"kala_vmdh": kala_additions,
|
|
"top_kala_support": [{"planet": planet, "virupas": value} for planet, value in top_kala],
|
|
"yuddha_bala": yuddha,
|
|
"active_yuddha": active_yuddha,
|
|
"sputa_drik_bala": drik_sputa,
|
|
"next_action": "若高级层与主排名冲突,先保留主 Shadbala 排名,再把冲突作为需要人工复核的证据。",
|
|
}
|
|
except Exception as exc:
|
|
logger.warning("[api_server] shadbala advanced layer failed: %s", exc)
|
|
return {}
|
|
|
|
|
|
def compute_shadbala(handler: Any, body: dict[str, Any]) -> dict[str, Any]:
|
|
import shadbala
|
|
|
|
planets, planet_lons, asc_sign_idx = handler._normalized_planets_from_body(body)
|
|
if not {"Sun", "Moon"} <= set(planet_lons):
|
|
raise ShadbalaInputError("planets must include Sun and Moon longitude data")
|
|
birth_hour = handler._get_float(body, "birth_hour", body.get("hour", 12), 0, 23)
|
|
birth_minute = handler._get_float(body, "birth_minute", body.get("minute", 0), 0, 59)
|
|
birth_second = handler._get_birth_second(body)
|
|
birth_hour_decimal = handler._birth_hour_decimal(birth_hour, birth_minute, birth_second)
|
|
context = birth_context(handler, body, birth_hour, birth_minute, birth_second)
|
|
result = shadbala.calc_shadbala(
|
|
planets,
|
|
SIGNS[asc_sign_idx],
|
|
birth_hour_decimal,
|
|
planet_lons["Sun"],
|
|
planet_lons["Moon"],
|
|
birth_minute,
|
|
context=context,
|
|
)
|
|
advanced = advanced_layer(handler, body, planets, result)
|
|
return {
|
|
"success": True,
|
|
"endpoint": "shadbala",
|
|
"rule_variants": rule_variants(bool(advanced)),
|
|
"advanced_layer": advanced,
|
|
"result": result,
|
|
}
|