"""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 @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), aktywnym silnikiem.""" 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) return build_chart(engine, moment, req.house_system) @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} @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