"""Modele domenowe silnika efemeryd — wspólny kontrakt dla KAŻDEGO silnika. To jest część LOG-28 (parzystość): każdy silnik (własny Skyfield czy zdalny swisseph) przyjmuje `ChartMoment` i zwraca listę `ObjectPosition` w identycznym kształcie i jednostkach. Dzięki temu wyniki są bezpośrednio porównywalne, a prezentacja/dane nie wiedzą, który silnik liczył. """ from __future__ import annotations from dataclasses import dataclass from datetime import datetime from typing import Any from app.engine import formats # kanoniczny zestaw i kolejność obiektów (LOG-02: światła + 7 klasycznych + 3 nowożytne) DEFAULT_OBJECTS = [ "Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto", ] @dataclass(frozen=True) class ChartMoment: """Wejście silnika: moment w UTC + lokalizacja geograficzna. Pozycje obiektów zależą tylko od czasu (geocentrycznie); szerokość/długość geograficzna będą potrzebne dopiero przy osiach i domach (LOG-05). """ when_utc: datetime # musi być świadome strefy (UTC) lat: float = 0.0 # szerokość geograficzna, + na północ lon: float = 0.0 # długość geograficzna, + na wschód @dataclass(frozen=True) class ObjectPosition: """Wynik dla jednego obiektu — surowe wartości w jednostkach SI astrologii.""" name: str longitude: float # długość ekliptyczna 0–360° (tropikalna, of-date) latitude: float # szerokość ekliptyczna (°) speed: float # prędkość w długości (°/dobę) retrograde: bool @property def sign(self) -> str: return formats.SIGNS[formats.sign_index(self.longitude)] @property def direction(self) -> str: return "Rx" if self.retrograde else "D" def as_dict(self) -> dict[str, Any]: return { "name": self.name, "sign": self.sign, "in_sign": formats.in_sign(self.longitude), "absolute": formats.absolute(self.longitude), "decimal": formats.decimal(self.longitude), "latitude": round(self.latitude, 6), "speed": round(self.speed, 6), "direction": self.direction, }