"""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 @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: """Pozycje obiektów dla danego momentu (LOG-01), liczone aktywnym silnikiem.""" from app.engine.models import ChartMoment engine = get_engine() moment = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon) positions = engine.positions(moment, req.objects) return {"engine": engine.name, "positions": [p.as_dict() for p in positions]} @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 @app.post("/chart/report") def chart_report(req: ReportRequest) -> dict: """Wynik obliczeń szukany w bazie: z pozycji generuje sygnifikatory i pyta warstwę danych o pasujące interpretacje (pierwsza wersja przepływu).""" 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) positions = engine.positions(moment) try: report = build_report(positions, DataClient(), per_object_limit=req.limit) except httpx.HTTPError as e: return {"engine": engine.name, "objects": [], "data_error": f"Warstwa danych niedostępna: {e}"} return {"engine": engine.name, **report} @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