mirror of
https://github.com/migatu/astrololo.git
synced 2026-07-14 21:38:37 +00:00
Silnik efemeryd: EngineProvider + SkyfieldEngine + harness porównawczy
Pierwszy increment implementacji warstwy logicznej (ścieżka A). LOG-24: interfejs EphemerisEngine z dwoma backendami — SkyfieldEngine (własny, permisywny: Skyfield MIT + dane JPL public domain) oraz RemoteEngine (klient izolowanej usługi swisseph). Fabryka + leniwa inicjalizacja; endpointy /chart/positions i /chart/compare. LOG-01: pozycje obiektów (długość/szerokość ekliptyczna, prędkość, kierunek, formaty: w znaku / absolutny / dziesiętny). LOG-25/28: harness porównawczy (compare.py) z progami tolerancji oraz wspólny kontrakt parzystości; pełen zestaw testów. LOG-27: services/engine-swisseph — osobna, opcjonalna usługa AGPL (pyswisseph, tryb Moshiera), licencjonowana osobno, w compose pod profilem "comparison"; nie wchodzi do zamkniętego produktu. Walidacja: SkyfieldEngine zgadza się ze Swiss Ephemeris co do ~1" dla wszystkich 10 obiektów na horoskopie referencyjnym (30.04.1984, Warszawa); 12 testów przechodzi (silnik B pomijany gdy nieskonfigurowany). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
"""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)}
|
||||
Reference in New Issue
Block a user