"""RemoteEngine — klient izolowanej usługi silnika (LOG-24 backend nr 2, LOG-27). Realizuje ten sam interfejs co SkyfieldEngine, ale liczenie deleguje przez HTTP do OSOBNEJ usługi `engine-swisseph` (AGPL). Dzięki granicy sieciowej kod AGPL nigdy nie jest linkowany do permisywnego produktu — patrz services/engine-swisseph. Używany tylko, gdy skonfigurowano ENGINE_SWISSEPH_URL (tryb porównawczy/dev/CI). """ from __future__ import annotations import os import httpx from app.engine.base import EphemerisEngine from app.engine.formats import norm360 from app.engine.models import DEFAULT_OBJECTS, ChartMoment, ObjectPosition class RemoteEngine(EphemerisEngine): name = "swisseph" def __init__(self, base_url: str | None = None, timeout: float = 15.0) -> None: self.base_url = (base_url or os.getenv("ENGINE_SWISSEPH_URL", "")).rstrip("/") self.timeout = timeout def positions( self, moment: ChartMoment, objects: list[str] | None = None ) -> list[ObjectPosition]: if not self.base_url: raise RuntimeError("ENGINE_SWISSEPH_URL nie ustawiony — silnik B niedostępny") payload = { "when_utc": moment.when_utc.isoformat(), "lat": moment.lat, "lon": moment.lon, "objects": objects or DEFAULT_OBJECTS, } with httpx.Client(timeout=self.timeout) as client: r = client.post(f"{self.base_url}/positions", json=payload) r.raise_for_status() rows = r.json()["positions"] return [ ObjectPosition( name=row["name"], longitude=norm360(row["longitude"]), latitude=row["latitude"], speed=row["speed"], retrograde=row["retrograde"], ) for row in rows ] def health(self) -> dict: if not self.base_url: return {"engine": self.name, "status": "disabled"} try: with httpx.Client(timeout=self.timeout) as client: r = client.get(f"{self.base_url}/health") r.raise_for_status() return {"engine": self.name, "status": "ok", "remote": r.json()} except httpx.HTTPError as e: return {"engine": self.name, "status": "down", "error": str(e)}