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
This commit is contained in:
Jesse_Chen
2026-09-29 14:59:08 +08:00
co-authored by Claude Fable 5.1
parent 15be6a0631
commit 7c5ea7ae5a
9 changed files with 516 additions and 18 deletions
+36
View File
@@ -43,9 +43,45 @@ def cmd_solar_return(args: Any) -> Dict[str, Any]:
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