diff --git a/lightserve/api/__init__.py b/lightserve/api/__init__.py index 3012b04..2f94464 100644 --- a/lightserve/api/__init__.py +++ b/lightserve/api/__init__.py @@ -10,6 +10,8 @@ from .lightcurves import lightcurves_router from .settings import settings from .sources import sources_router +from .analysis import analysis_router + app = FastAPI() @@ -27,3 +29,4 @@ app.include_router(lightcurves_router) app.include_router(sources_router) app.include_router(cutouts_router) +app.include_router(analysis_router) diff --git a/lightserve/api/analysis.py b/lightserve/api/analysis.py new file mode 100644 index 0000000..a1a6eb1 --- /dev/null +++ b/lightserve/api/analysis.py @@ -0,0 +1,164 @@ +""" +Endpoints for lightcurve statistics analysis. +""" + +from datetime import datetime +from typing import Optional + +from fastapi import APIRouter, HTTPException, Request, status, Query +from pydantic import BaseModel + +from lightserve.database import AsyncSessionDependency +from lightcurvedb.models.analysis import BandStatistics, BandTimeSeries +from lightcurvedb.client.source import SourceNotFound, source_read +from lightcurvedb.client.band import BandNotFound, band_read +from lightcurvedb.analysis.statistics import get_band_statistics, get_band_timeseries + + +from .auth import requires + +analysis_router = APIRouter(prefix="/analysis") + + +class BandStatisticsResponse(BaseModel): + """ + Response model for band statistics + """ + source_id: int + band_name: str + statistics: BandStatistics + start_time: datetime | None + end_time: datetime | None + time_resolution: str + + +class BandTimeSeriesResponse(BaseModel): + """ + Response model for band timeseries + """ + source_id: int + band_name: str + timeseries: BandTimeSeries + time_resolution: str + + + +@analysis_router.get("/aggregate/{source_id}/{band_name}") +@requires("lcs:read") +async def get_source_band_statistics( + request: Request, + source_id: int, + band_name: str, + conn: AsyncSessionDependency, + start_time: Optional[datetime] = Query( + None, + description="Start time for statistics calculation (YYYY-MM-DD)" + ), + end_time: Optional[datetime] = Query( + None, + description="End time for statistics calculation (YYYY-MM-DD)" + ), +) -> BandStatisticsResponse: + """ + Calculate statistical measures for a specific source and band. + + By default, uses continuous aggregate tables. + """ + try: + await source_read(id=source_id, conn=conn) + except SourceNotFound: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Source {source_id} not found" + ) + + try: + await band_read(band_name, conn=conn) + except BandNotFound: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Band '{band_name}' not found" + ) + + # Validate time range + if start_time and end_time and start_time >= end_time: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="start_time must be before end_time" + ) + + statistics, bucket_start, bucket_end, time_resolution = await get_band_statistics( + source_id=source_id, + band_name=band_name, + conn=conn, + start_time=start_time, + end_time=end_time + ) + + return BandStatisticsResponse( + source_id=source_id, + band_name=band_name, + statistics=statistics, + start_time=bucket_start, + end_time=bucket_end, + time_resolution=time_resolution + ) + + +@analysis_router.get("/timeseries/{source_id}/{band_name}") +@requires("lcs:read") +async def get_source_band_timeseries( + request: Request, + source_id: int, + band_name: str, + conn: AsyncSessionDependency, + start_time: Optional[datetime] = Query( + None, + description="Start time for timeseries (YYYY-MM-DD)" + ), + end_time: Optional[datetime] = Query( + None, + description="End time for timeseries (YYYY-MM-DD)" + ), +) -> BandTimeSeriesResponse: + """ + Get timeseries of per bucket for a specific source and band + """ + try: + await source_read(id=source_id, conn=conn) + except SourceNotFound: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Source {source_id} not found" + ) + + try: + await band_read(band_name, conn=conn) + except BandNotFound: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Band '{band_name}' not found" + ) + + # Validate time range + if start_time and end_time and start_time >= end_time: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="start_time must be before end_time" + ) + + timeseries, time_resolution = await get_band_timeseries( + source_id=source_id, + band_name=band_name, + conn=conn, + start_time=start_time, + end_time=end_time + ) + + return BandTimeSeriesResponse( + source_id=source_id, + band_name=band_name, + timeseries=timeseries, + time_resolution=time_resolution + ) +