Files
Jyotisha/scripts/cmd_solar_return.py
T
Jesse_ChenandClaude Fable 5.1 7c5ea7ae5a feat(annual): localise the solar-return instant, relocated returns, compact PL9 export (BUG-1095..1097)
Upstream yinduzhanxing 41d1c740 / 59c9139f, hand-merged; 089858c0 verified not applicable.

- solar_return: dt_local was the UT instant verbatim (no caller ever added the
  offset). Localise with ZoneInfo at the return instant (IANA id) or the fixed
  offset; expose annual_location; Sahams day/night at the true instant;
  optional current_location return charts.
- cmd_solar_return / annual_tajika_pack / engine: pass birth IANA zone and
  --solar-return-location-mode / --current-* through; external PyJHora
  replay blocked when relocated.
- output_json keeps the upstream stream= signature; PL9 exports written
  compact via dumps (measured faster than json.dump streaming), 9.57 -> 4.61 MB.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
2026-09-29 14:59:08 +08:00

105 lines
4.2 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
cmd_solar_return: Solar Return (Varshaphala) CLI 子命令
Jyotish Vedic Astrology Skill — v6.0.18
用法:
python3 jyotish_engine.py solar-return --year REDACTED_YEAR --month 4 --day 17 \
--hour 14 --minute 45 --lat 36.4667 --lon 114.2 --tz 8 --target-year 2026
"""
import sys
import os
from typing import Dict, Any
def cmd_solar_return(args: Any) -> Dict[str, Any]:
"""
solar-return 子命令:计算太阳返照盘 + 完整 Varshaphala 分析。
参数 (来自 args):
year, month, day, hour, minute: 出生时间
lat, lon, tz: 出生地点/时区
target_year: 目标年份(计算该年太阳返照)
返回:
dict: solar_return_full_report() 的结果
"""
try:
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from ayanamsa_utils import DEFAULT_AYANAMSA_NAME
from solar_return import solar_return_full_report
except ImportError as e:
return {"error": f"solar_return 模块导入失败: {e}"}
if not hasattr(args, 'target_year') or args.target_year is None:
return {"error": "solar-return 需要 --target-year 参数",
"hint": "指定目标年份,如 --target-year 2026"}
return solar_return_full_report(
args.year, args.month, args.day,
args.hour, args.minute,
args.lat, args.lon, args.tz,
args.target_year,
ayanamsa_name=getattr(args, 'ayanamsa', None) or DEFAULT_AYANAMSA_NAME,
**solar_return_location_kwargs(args),
)
def solar_return_location_kwargs(args: Any) -> Dict[str, Any]:
"""Return-location kwargs for ``solar_return_full_report``.
Two arg shapes are accepted: the annual-pack namespace (``return_lat`` /
``return_lon`` / ``return_tz`` / ``return_timezone_id``, built by
``annual_tajika_pack._args_from_payload``) and the CLI namespace
(``--solar-return-location-mode`` / ``--current-*`` / ``--timezone-id``).
Upstream yinduzhanxing 41d1c740.
"""
if getattr(args, 'return_lat', None) is not None or getattr(args, 'return_timezone_id', None):
return {
'local_timezone_id': getattr(args, 'return_timezone_id', None),
'return_lat': getattr(args, 'return_lat', None),
'return_lon': getattr(args, 'return_lon', None),
'return_tz': getattr(args, 'return_tz', None),
}
mode = getattr(args, 'solar_return_location_mode', None) or 'birth_place'
if mode == 'current_location':
if any(getattr(args, key, None) is None for key in ('current_lat', 'current_lon', 'current_tz')):
raise ValueError('current_location requires current_lat, current_lon and current_tz')
return {
'local_timezone_id': getattr(args, 'current_timezone_id', None),
'return_lat': float(args.current_lat),
'return_lon': float(args.current_lon),
'return_tz': float(args.current_tz),
}
# CLI passes ``--timezone-id``; API payloads reach here as ``timezone`` /
# ``timezone_name`` (calculation_profile_contract keys). Only IANA ids count.
for key in ('timezone_id', 'timezone', 'timezone_name'):
value = getattr(args, key, None)
if isinstance(value, str) and '/' in value:
return {'local_timezone_id': value.strip()}
return {'local_timezone_id': None}
if __name__ == '__main__':
# 独立测试入口
import argparse
from datetime import datetime
parser = argparse.ArgumentParser(description='Solar Return 测试')
parser.add_argument('--year', type=int, required=True)
parser.add_argument('--month', type=int, required=True)
parser.add_argument('--day', type=int, required=True)
parser.add_argument('--hour', type=int, default=12)
parser.add_argument('--minute', type=int, default=0)
parser.add_argument('--lat', type=float, required=True)
parser.add_argument('--lon', type=float, required=True)
parser.add_argument('--tz', type=float, default=0.0)
parser.add_argument('--target-year', type=int, required=True)
a = parser.parse_args()
result = cmd_solar_return(a)
import json
print(json.dumps(result, ensure_ascii=False, indent=2, default=str))