mirror of
https://github.com/migatu/astrololo.git
synced 2026-07-14 21:38:37 +00:00
2dbd3410e9
engine/timeline.py: scala w jedną posortowaną oś czasu (technique | significator | start | exact | end): - profekcje roczne (LOG-10) — Władca Roku per rok życia, - Solar Return (LOG-12) — moment powrotu Słońca, - dyrekcje solar-arc — daty dokładnych aspektów kierowanych planet do punktów natalnych (klucz Naiboda 0°59'08"/rok, konfigurowalny; okno orbowe ±1 rok). Endpoint /chart/timeline (zakres from_date..to_date, wybór technik). Walidacja: - profekcje spójne z tabelą notes3; dyrekcja Sun koniunkcja MC (łuk 312°) słusznie poza życiem; oś posortowana po dacie dokładnej; wiek = łuk/klucz spójny z datą. - E2E: dla 2025-2026 zwraca 19 zdarzeń (m.in. Władca Roku 42 Saturn 30.04.2026, Solar Return, dyr. Venus koniunkcja North Node 27.05.2025). 66 testów przechodzi. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
211 lines
7.5 KiB
Python
211 lines
7.5 KiB
Python
"""Warstwa LOGICZNA — usługa HTTP.
|
|
|
|
W górę: udostępnia API dla warstwy prezentacji.
|
|
W dół: woła warstwę bazodanową (DataClient).
|
|
Nie serwuje HTML, nie czyta plików/baz — tylko reguły i pośrednictwo.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
import httpx
|
|
from fastapi import FastAPI, HTTPException
|
|
from pydantic import BaseModel
|
|
|
|
from app.clients.data_client import DataClient
|
|
from app.models import QueryRequest, QueryResponse
|
|
from app.service import QueryService
|
|
|
|
app = FastAPI(title="astrololo · warstwa logiczna")
|
|
service = QueryService()
|
|
|
|
# --- silnik efemeryd (LOG-24): budowany leniwie, by nie wymagać Skyfielda do startu ---
|
|
_engine = None
|
|
|
|
|
|
def get_engine():
|
|
global _engine
|
|
if _engine is None:
|
|
from app.engine.factory import build_engine
|
|
|
|
_engine = build_engine()
|
|
return _engine
|
|
|
|
|
|
class PositionsRequest(BaseModel):
|
|
when_utc: datetime # moment w UTC (świadomy strefy)
|
|
lat: float = 0.0
|
|
lon: float = 0.0
|
|
objects: list[str] | None = None
|
|
house_system: str = "whole_sign" # whole_sign | equal | porphyry
|
|
stations: bool = False # licz stacje (LOG-03; wolniejsze — root-findy)
|
|
|
|
|
|
@app.post("/api/query", response_model=QueryResponse)
|
|
def query(req: QueryRequest) -> QueryResponse:
|
|
try:
|
|
return service.handle(req)
|
|
except httpx.HTTPError as e:
|
|
raise HTTPException(status_code=502, detail=f"Warstwa bazodanowa niedostępna: {e}")
|
|
|
|
|
|
@app.post("/chart/positions")
|
|
def chart_positions(req: PositionsRequest) -> dict:
|
|
"""Pełny horoskop: pozycje (LOG-01) + osie i domy (LOG-05) + aspekty (LOG-06);
|
|
opcjonalnie stacje planet (LOG-03, stations=true)."""
|
|
from app.engine.chart import build_chart
|
|
from app.engine.models import ChartMoment
|
|
|
|
engine = get_engine()
|
|
moment = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon)
|
|
chart = build_chart(engine, moment, req.house_system)
|
|
if req.stations:
|
|
from app.engine.stations import find_stations
|
|
|
|
for p in chart["positions"]:
|
|
st = find_stations(engine, moment, p["name"])
|
|
if st:
|
|
p["stations"] = st
|
|
return chart
|
|
|
|
|
|
@app.post("/chart/compare")
|
|
def chart_compare(req: PositionsRequest) -> dict:
|
|
"""Tryb dwu-silnikowy (LOG-26): policz oboma silnikami i zwróć raport różnic.
|
|
|
|
Wymaga skonfigurowanego ENGINE_SWISSEPH_URL (silnik B). W przeciwnym razie
|
|
zwraca informację, że porównanie jest niedostępne.
|
|
"""
|
|
from app.engine.compare import compare_engines
|
|
from app.engine.factory import build_engine
|
|
from app.engine.models import ChartMoment
|
|
|
|
moment = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon)
|
|
try:
|
|
report = compare_engines(build_engine("own"), build_engine("swisseph"), moment)
|
|
except (RuntimeError, httpx.HTTPError) as e:
|
|
raise HTTPException(status_code=503, detail=f"Silnik B niedostępny: {e}")
|
|
return report.summary()
|
|
|
|
|
|
class ReportRequest(BaseModel):
|
|
when_utc: datetime
|
|
lat: float = 0.0
|
|
lon: float = 0.0
|
|
limit: int = 5000
|
|
group: bool = False # grupowanie identycznych opisów
|
|
|
|
|
|
@app.post("/chart/report")
|
|
def chart_report(req: ReportRequest) -> dict:
|
|
"""Wynik obliczeń szukany w bazie: z pozycji + domów + aspektów generuje
|
|
sygnifikatory (fasety znak/dom/aspekt) i pyta warstwę danych o interpretacje."""
|
|
from app.engine.chart import build_chart
|
|
from app.engine.models import ChartMoment
|
|
from app.significators import build_report
|
|
|
|
engine = get_engine()
|
|
moment = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon)
|
|
chart = build_chart(engine, moment) # pozycje z domami + aspekty
|
|
try:
|
|
report = build_report(
|
|
chart["positions"], DataClient(),
|
|
aspects=chart.get("aspects"), per_object_limit=req.limit, group=req.group,
|
|
)
|
|
except httpx.HTTPError as e:
|
|
return {"engine": engine.name, "objects": [], "data_error": f"Warstwa danych niedostępna: {e}"}
|
|
return {"engine": engine.name, **report}
|
|
|
|
|
|
class ProfectionsRequest(BaseModel):
|
|
when_utc: datetime # moment urodzenia (UTC)
|
|
lat: float = 0.0
|
|
lon: float = 0.0
|
|
start_age: int = 0
|
|
count: int = 13 # domyślnie pełny cykl 12 lat + rok startowy
|
|
|
|
|
|
@app.post("/chart/profections")
|
|
def chart_profections(req: ProfectionsRequest) -> dict:
|
|
"""Profekcje roczne (LOG-10): wiek, profektowany Asc, Władca Roku (+MC/Su/Mo)."""
|
|
from app.engine import houses as H
|
|
from app.engine.models import ChartMoment
|
|
from app.engine.profections import profection_rows
|
|
|
|
engine = get_engine()
|
|
natal = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon)
|
|
ramc, eps = engine.sidereal(natal)
|
|
points = {
|
|
"Asc": H.compute_asc(ramc, eps, natal.lat),
|
|
"MC": H.compute_mc(ramc, eps),
|
|
}
|
|
for p in engine.positions(natal, ["Sun", "Moon"]):
|
|
points[p.name] = p.longitude
|
|
rows = profection_rows(points, req.when_utc, req.start_age, req.count)
|
|
return {"engine": engine.name, "rows": rows}
|
|
|
|
|
|
class ReturnRequest(BaseModel):
|
|
when_utc: datetime # moment urodzenia (UTC)
|
|
lat: float = 0.0
|
|
lon: float = 0.0
|
|
kind: str = "solar" # solar | lunar
|
|
around: datetime | None = None # data, wokół której szukać powrotu
|
|
|
|
|
|
@app.post("/chart/return")
|
|
def chart_return(req: ReturnRequest) -> dict:
|
|
"""Solar/Lunar Return (LOG-12): moment powrotu + pełny horoskop na ten moment."""
|
|
from app.engine.chart import build_chart
|
|
from app.engine.models import ChartMoment
|
|
from app.engine.returns import find_return
|
|
|
|
if req.kind not in ("solar", "lunar"):
|
|
raise HTTPException(status_code=422, detail="kind: solar albo lunar")
|
|
engine = get_engine()
|
|
natal = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon)
|
|
around = req.around or req.when_utc
|
|
hit = find_return(engine, req.kind, natal, around)
|
|
if hit is None:
|
|
raise HTTPException(status_code=404, detail="nie znaleziono powrotu w oknie skanu")
|
|
chart = build_chart(engine, ChartMoment(when_utc=hit, lat=req.lat, lon=req.lon))
|
|
return {"engine": engine.name, "kind": req.kind,
|
|
"return_utc": hit.isoformat(), **chart}
|
|
|
|
|
|
class TimelineRequest(BaseModel):
|
|
when_utc: datetime # moment urodzenia (UTC)
|
|
lat: float = 0.0
|
|
lon: float = 0.0
|
|
from_date: str # zakres: YYYY-MM-DD
|
|
to_date: str
|
|
techniques: list[str] | None = None # profection | solar_return | solar_arc
|
|
|
|
|
|
@app.post("/chart/timeline")
|
|
def chart_timeline(req: TimelineRequest) -> dict:
|
|
"""Zbiorcza oś czasu z technik (LOG-14): technique | significator | start | exact | end."""
|
|
from app.engine import houses as H
|
|
from app.engine.models import ChartMoment
|
|
from app.engine.timeline import build_timeline
|
|
|
|
engine = get_engine()
|
|
natal = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon)
|
|
ramc, eps = engine.sidereal(natal)
|
|
points = {"Asc": H.compute_asc(ramc, eps, natal.lat), "MC": H.compute_mc(ramc, eps)}
|
|
for p in engine.positions(natal):
|
|
points[p.name] = p.longitude
|
|
events = build_timeline(engine, natal, points, req.from_date, req.to_date, req.techniques)
|
|
return {"engine": engine.name, "from": req.from_date, "to": req.to_date,
|
|
"count": len(events), "events": events}
|
|
|
|
|
|
@app.get("/health")
|
|
def health() -> dict:
|
|
info = {"status": "ok", "layer": "logic"}
|
|
try:
|
|
info["data_layer"] = DataClient().health()
|
|
except httpx.HTTPError as e:
|
|
info["data_layer"] = {"status": "down", "error": str(e)}
|
|
return info
|