|
|
@@ -69,6 +69,17 @@ def _parse_date(date_str: str) -> float:
|
|
|
return julday(dt.year, dt.month, dt.day, 12.0)
|
|
|
|
|
|
|
|
|
+def _utc_date_from_datetime(dt_str: str | None) -> str:
|
|
|
+ """Convert an ISO datetime string to a UTC date string."""
|
|
|
+ if dt_str is None:
|
|
|
+ return __import__("datetime").datetime.now(timezone.utc).date().isoformat()
|
|
|
+
|
|
|
+ dt = datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
|
|
|
+ if dt.tzinfo is None:
|
|
|
+ dt = dt.replace(tzinfo=timezone.utc)
|
|
|
+ return dt.astimezone(timezone.utc).date().isoformat()
|
|
|
+
|
|
|
+
|
|
|
def _default_location(lat: float | None = None, lon: float | None = None) -> tuple[float, float]:
|
|
|
resolved_lat = getattr(config, "DEFAULT_LAT", None) if lat is None else lat
|
|
|
resolved_lon = getattr(config, "DEFAULT_LON", None) if lon is None else lon
|
|
|
@@ -84,6 +95,7 @@ def _tool_names() -> list[str]:
|
|
|
"get_sidereal_time",
|
|
|
"get_constellation_at_ecliptic",
|
|
|
"list_available_bodies",
|
|
|
+ "get_sky_state",
|
|
|
]
|
|
|
|
|
|
|
|
|
@@ -95,7 +107,7 @@ def get_planetary_positions(
|
|
|
lat: float | None = None,
|
|
|
lon: float | None = None,
|
|
|
elevation: float = 0.0,
|
|
|
- geocentric: bool = False,
|
|
|
+ geocentric: bool = True,
|
|
|
) -> dict:
|
|
|
"""
|
|
|
Get positions of all major solar system bodies.
|
|
|
@@ -113,23 +125,50 @@ def get_planetary_positions(
|
|
|
lat, lon = _default_location(lat, lon)
|
|
|
jd = _parse_datetime(datetime)
|
|
|
|
|
|
- ck = cache_key("get_planetary_positions", datetime=datetime or "now", lat=lat, lon=lon, geocentric=geocentric)
|
|
|
+ ck = cache_key(
|
|
|
+ "get_planetary_positions",
|
|
|
+ cache_version=3,
|
|
|
+ datetime=datetime or "now",
|
|
|
+ elevation=elevation,
|
|
|
+ geocentric=geocentric,
|
|
|
+ lat=lat,
|
|
|
+ lon=lon,
|
|
|
+ )
|
|
|
cached = cache.get(ck)
|
|
|
if cached:
|
|
|
logger.info(f"cache hit: {ck}")
|
|
|
- return {"input": {"datetime": datetime, "lat": lat, "lon": lon, "geocentric": geocentric}, **cached}
|
|
|
+ return {
|
|
|
+ "input": {
|
|
|
+ "datetime": datetime,
|
|
|
+ "lat": lat,
|
|
|
+ "lon": lon,
|
|
|
+ "elevation": elevation,
|
|
|
+ "geocentric": geocentric,
|
|
|
+ },
|
|
|
+ **cached,
|
|
|
+ }
|
|
|
|
|
|
positions = _compute_planetary_positions(jd, lat, lon, elevation, geocentric)
|
|
|
|
|
|
result = {
|
|
|
- "input": {"datetime": datetime, "lat": lat, "lon": lon, "geocentric": geocentric},
|
|
|
+ "input": {
|
|
|
+ "datetime": datetime,
|
|
|
+ "lat": lat,
|
|
|
+ "lon": lon,
|
|
|
+ "elevation": elevation,
|
|
|
+ "geocentric": geocentric,
|
|
|
+ },
|
|
|
"timestamp_utc": datetime or __import__("datetime").datetime.now(timezone.utc).isoformat(),
|
|
|
"julian_day": round(jd, 6),
|
|
|
"bodies": positions,
|
|
|
}
|
|
|
cache.set(
|
|
|
ck,
|
|
|
- {"timestamp_utc": result["timestamp_utc"], "julian_day": result["julian_day"], "bodies": positions},
|
|
|
+ {
|
|
|
+ "timestamp_utc": result["timestamp_utc"],
|
|
|
+ "julian_day": result["julian_day"],
|
|
|
+ "bodies": positions,
|
|
|
+ },
|
|
|
config.CACHE_PLANETARY,
|
|
|
)
|
|
|
return result
|
|
|
@@ -196,7 +235,7 @@ def get_lunar_state(
|
|
|
lat, lon = _default_location(lat, lon)
|
|
|
jd = _parse_datetime(datetime)
|
|
|
|
|
|
- ck = cache_key("get_lunar_state", datetime=datetime or "now", lat=lat, lon=lon)
|
|
|
+ ck = cache_key("get_lunar_state", cache_version=3, datetime=datetime or "now", lat=lat, lon=lon)
|
|
|
cached = cache.get(ck)
|
|
|
cached_next_major_phase = cached.get("lunar_state", {}).get("next_major_phase") if cached else None
|
|
|
if cached_next_major_phase and cached_next_major_phase.get("phase_name") and cached_next_major_phase.get("at_utc") and cached_next_major_phase.get("in_text"):
|
|
|
@@ -327,10 +366,96 @@ def get_moon_phase(datetime: str | None = None, lat: float | None = None, lon: f
|
|
|
}
|
|
|
|
|
|
|
|
|
+@mcp.tool()
|
|
|
+def get_sky_state(
|
|
|
+ datetime: str | None = None,
|
|
|
+ lat: float | None = None,
|
|
|
+ lon: float | None = None,
|
|
|
+ elevation: float = 0.0,
|
|
|
+ geocentric: bool = True,
|
|
|
+) -> dict:
|
|
|
+ """
|
|
|
+ Return a consolidated raw sky-state snapshot for downstream chart engines.
|
|
|
+
|
|
|
+ This stays purely descriptive: it combines the core astronomical outputs
|
|
|
+ already exposed elsewhere in the server without adding signs, houses, or
|
|
|
+ interpretation.
|
|
|
+ """
|
|
|
+ lat, lon = _default_location(lat, lon)
|
|
|
+ date = _utc_date_from_datetime(datetime)
|
|
|
+ jd = _parse_datetime(datetime)
|
|
|
+
|
|
|
+ ck = cache_key(
|
|
|
+ "get_sky_state",
|
|
|
+ cache_version=1,
|
|
|
+ datetime=datetime or "now",
|
|
|
+ date=date,
|
|
|
+ elevation=elevation,
|
|
|
+ geocentric=geocentric,
|
|
|
+ lat=lat,
|
|
|
+ lon=lon,
|
|
|
+ )
|
|
|
+ cached = cache.get(ck)
|
|
|
+ if cached:
|
|
|
+ return {
|
|
|
+ "input": {
|
|
|
+ "datetime": datetime,
|
|
|
+ "date": date,
|
|
|
+ "lat": lat,
|
|
|
+ "lon": lon,
|
|
|
+ "elevation": elevation,
|
|
|
+ "geocentric": geocentric,
|
|
|
+ },
|
|
|
+ **cached,
|
|
|
+ }
|
|
|
+
|
|
|
+ planetary_positions = get_planetary_positions(
|
|
|
+ datetime=datetime,
|
|
|
+ lat=lat,
|
|
|
+ lon=lon,
|
|
|
+ elevation=elevation,
|
|
|
+ geocentric=geocentric,
|
|
|
+ )
|
|
|
+ lunar_state = get_lunar_state(datetime=datetime, lat=lat, lon=lon)
|
|
|
+ sidereal_time = get_sidereal_time(datetime=datetime, lat=lat, lon=lon)
|
|
|
+ solar_events = get_solar_events(date=date, lat=lat, lon=lon)
|
|
|
+
|
|
|
+ result = {
|
|
|
+ "input": {
|
|
|
+ "datetime": datetime,
|
|
|
+ "date": date,
|
|
|
+ "lat": lat,
|
|
|
+ "lon": lon,
|
|
|
+ "elevation": elevation,
|
|
|
+ "geocentric": geocentric,
|
|
|
+ },
|
|
|
+ "timestamp_utc": datetime or __import__("datetime").datetime.now(timezone.utc).isoformat(),
|
|
|
+ "julian_day": round(jd, 6),
|
|
|
+ "planetary_positions": planetary_positions,
|
|
|
+ "solar_events": solar_events,
|
|
|
+ "lunar_state": lunar_state,
|
|
|
+ "sidereal_time": sidereal_time,
|
|
|
+ }
|
|
|
+ cache.set(
|
|
|
+ ck,
|
|
|
+ {
|
|
|
+ "timestamp_utc": result["timestamp_utc"],
|
|
|
+ "julian_day": result["julian_day"],
|
|
|
+ "planetary_positions": planetary_positions,
|
|
|
+ "solar_events": solar_events,
|
|
|
+ "lunar_state": lunar_state,
|
|
|
+ "sidereal_time": sidereal_time,
|
|
|
+ },
|
|
|
+ config.CACHE_SKY,
|
|
|
+ )
|
|
|
+ return result
|
|
|
+
|
|
|
+
|
|
|
def create_app() -> FastAPI:
|
|
|
"""
|
|
|
Build the FastAPI app for the v0.1 core slice.
|
|
|
"""
|
|
|
+ config.LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
logging.basicConfig(
|
|
|
filename=str(config.LOG_DIR / "server.log"),
|
|
|
level=logging.INFO,
|