From 7f204eba3ab16186a93b7a82c46d5ae6ab74bd48 Mon Sep 17 00:00:00 2001 From: Divesh Date: Fri, 29 Aug 2025 14:18:02 +0100 Subject: [PATCH 1/7] Added preliminary api endpoint --- lightserve/api/__init__.py | 3 + lightserve/api/analysis.py | 142 +++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 lightserve/api/analysis.py 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..e03ad81 --- /dev/null +++ b/lightserve/api/analysis.py @@ -0,0 +1,142 @@ +""" +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, Field + +from lightserve.database import AsyncSessionDependency +from lightcurvedb.models.analysis import BandStatistics +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_statistics_wo_ca + + +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 + + + +@analysis_router.get("/{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" + ), + end_time: Optional[datetime] = Query( + None, + description="End time for statistics calculation" + ), +) -> BandStatisticsResponse: + """ + Calculate statistical measures 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 provided + 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 = 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 + ) + + +@analysis_router.get("/wo_ca/{source_id}/{band_name}") +@requires("lcs:read") +async def get_source_band_statisticsx( + request: Request, + source_id: int, + band_name: str, + conn: AsyncSessionDependency, + start_time: Optional[datetime] = Query( + None, + description="Start time for statistics calculation (ISO format)" + ), + end_time: Optional[datetime] = Query( + None, + description="End time for statistics calculation (ISO format)" + ), +) -> BandStatisticsResponse: + """ + Calculate statistical measures 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 provided + 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 = await get_band_statistics_wo_ca( + 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 + ) \ No newline at end of file From a6d80d9a089e44dd3a905decde7e98250f48052a Mon Sep 17 00:00:00 2001 From: Divesh Date: Wed, 17 Sep 2025 15:37:34 +0100 Subject: [PATCH 2/7] minor update --- lightserve/api/analysis.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lightserve/api/analysis.py b/lightserve/api/analysis.py index e03ad81..3cec69b 100644 --- a/lightserve/api/analysis.py +++ b/lightserve/api/analysis.py @@ -45,7 +45,7 @@ async def get_source_band_statistics( ), ) -> BandStatisticsResponse: """ - Calculate statistical measures for a specific source and band. + Calculate statistical measures for a specific source and band using Continuous Aggregate Table """ try: await source_read(id=source_id, conn=conn) @@ -87,7 +87,7 @@ async def get_source_band_statistics( @analysis_router.get("/wo_ca/{source_id}/{band_name}") @requires("lcs:read") -async def get_source_band_statisticsx( +async def get_source_band_statistics_without_continuous_aggregates( request: Request, source_id: int, band_name: str, @@ -102,7 +102,7 @@ async def get_source_band_statisticsx( ), ) -> BandStatisticsResponse: """ - Calculate statistical measures for a specific source and band. + Calculate statistical measures for a specific source and band using FluxMeasurementTable (Not for production) """ try: await source_read(id=source_id, conn=conn) From da014890c3bd083d69047a03c78797ae7338fce2 Mon Sep 17 00:00:00 2001 From: Divesh Date: Thu, 2 Oct 2025 12:47:45 +0100 Subject: [PATCH 3/7] updated the responsebody for statistics results --- lightserve/api/analysis.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/lightserve/api/analysis.py b/lightserve/api/analysis.py index 3cec69b..8d346f0 100644 --- a/lightserve/api/analysis.py +++ b/lightserve/api/analysis.py @@ -25,6 +25,8 @@ class BandStatisticsResponse(BaseModel): source_id: int band_name: str statistics: BandStatistics + start_time: datetime + end_time: datetime @@ -37,11 +39,11 @@ async def get_source_band_statistics( conn: AsyncSessionDependency, start_time: Optional[datetime] = Query( None, - description="Start time for statistics calculation" + description="Start time for statistics calculation (YYYY-MM-DD)" ), end_time: Optional[datetime] = Query( None, - description="End time for statistics calculation" + description="End time for statistics calculation (YYYY-MM-DD)" ), ) -> BandStatisticsResponse: """ @@ -63,25 +65,27 @@ async def get_source_band_statistics( detail=f"Band '{band_name}' not found" ) - # Validate time range if provided + # 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 = await get_band_statistics( + + statistics, bucket_start, bucket_end = 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 + statistics=statistics, + start_time=bucket_start, + end_time=bucket_end ) @@ -120,23 +124,25 @@ async def get_source_band_statistics_without_continuous_aggregates( detail=f"Band '{band_name}' not found" ) - # Validate time range if provided + # 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 = await get_band_statistics_wo_ca( + + statistics, _, _ = await get_band_statistics_wo_ca( 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 + statistics=statistics, + start_time=start_time, + end_time=end_time ) \ No newline at end of file From 515d0031085da39315968cbfbd5a2b2374ae49db Mon Sep 17 00:00:00 2001 From: Divesh Date: Fri, 3 Oct 2025 12:02:23 +0100 Subject: [PATCH 4/7] passing time resolution as part of the response --- lightserve/api/analysis.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lightserve/api/analysis.py b/lightserve/api/analysis.py index 8d346f0..16cb279 100644 --- a/lightserve/api/analysis.py +++ b/lightserve/api/analysis.py @@ -6,7 +6,7 @@ from typing import Optional from fastapi import APIRouter, HTTPException, Request, status, Query -from pydantic import BaseModel, Field +from pydantic import BaseModel from lightserve.database import AsyncSessionDependency from lightcurvedb.models.analysis import BandStatistics @@ -21,12 +21,15 @@ class BandStatisticsResponse(BaseModel): - """Response model for band statistics""" + """ + Response model for band statistics + """ source_id: int band_name: str statistics: BandStatistics - start_time: datetime + start_time: datetime end_time: datetime + time_resolution: str @@ -72,7 +75,7 @@ async def get_source_band_statistics( detail="start_time must be before end_time" ) - statistics, bucket_start, bucket_end = await get_band_statistics( + statistics, bucket_start, bucket_end, time_resolution = await get_band_statistics( source_id=source_id, band_name=band_name, conn=conn, @@ -85,7 +88,8 @@ async def get_source_band_statistics( band_name=band_name, statistics=statistics, start_time=bucket_start, - end_time=bucket_end + end_time=bucket_end, + time_resolution=time_resolution ) @@ -144,5 +148,6 @@ async def get_source_band_statistics_without_continuous_aggregates( band_name=band_name, statistics=statistics, start_time=start_time, - end_time=end_time + end_time=end_time, + time_resolution="daily" ) \ No newline at end of file From b634a1b6c15fb7e492e6a1a395b7f93299ce5840 Mon Sep 17 00:00:00 2001 From: Divesh Date: Fri, 3 Oct 2025 16:45:32 +0100 Subject: [PATCH 5/7] added time series field and variance parameter --- lightserve/api/analysis.py | 78 +++++++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/lightserve/api/analysis.py b/lightserve/api/analysis.py index 16cb279..4e93e07 100644 --- a/lightserve/api/analysis.py +++ b/lightserve/api/analysis.py @@ -9,10 +9,10 @@ from pydantic import BaseModel from lightserve.database import AsyncSessionDependency -from lightcurvedb.models.analysis import BandStatistics +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_statistics_wo_ca +from lightcurvedb.analysis.statistics import get_band_statistics, get_band_statistics_wo_ca, get_band_timeseries from .auth import requires @@ -27,13 +27,22 @@ class BandStatisticsResponse(BaseModel): source_id: int band_name: str statistics: BandStatistics - start_time: datetime - end_time: datetime + 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 -@analysis_router.get("/{source_id}/{band_name}") + + +@analysis_router.get("/aggregate/{source_id}/{band_name}") @requires("lcs:read") async def get_source_band_statistics( request: Request, @@ -93,7 +102,64 @@ async def get_source_band_statistics( ) -@analysis_router.get("/wo_ca/{source_id}/{band_name}") +@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 = 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 + ) + + +@analysis_router.get("/wo_ca/aggregate/{source_id}/{band_name}") @requires("lcs:read") async def get_source_band_statistics_without_continuous_aggregates( request: Request, From df967f54de7acab420cbcab1940e366b86a3b52c Mon Sep 17 00:00:00 2001 From: Divesh Date: Wed, 15 Oct 2025 13:04:59 +0100 Subject: [PATCH 6/7] handling without_aggregate requests within the same structure as aggregate requests --- lightserve/api/analysis.py | 69 ++++---------------------------------- 1 file changed, 6 insertions(+), 63 deletions(-) diff --git a/lightserve/api/analysis.py b/lightserve/api/analysis.py index 4e93e07..e7035d4 100644 --- a/lightserve/api/analysis.py +++ b/lightserve/api/analysis.py @@ -12,7 +12,7 @@ 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_statistics_wo_ca, get_band_timeseries +from lightcurvedb.analysis.statistics import get_band_statistics, get_band_timeseries from .auth import requires @@ -50,16 +50,18 @@ async def get_source_band_statistics( band_name: str, conn: AsyncSessionDependency, start_time: Optional[datetime] = Query( - None, + None, description="Start time for statistics calculation (YYYY-MM-DD)" ), end_time: Optional[datetime] = Query( - None, + None, description="End time for statistics calculation (YYYY-MM-DD)" ), ) -> BandStatisticsResponse: """ - Calculate statistical measures for a specific source and band using Continuous Aggregate Table + Calculate statistical measures for a specific source and band. + + By default, uses continuous aggregate tables. """ try: await source_read(id=source_id, conn=conn) @@ -158,62 +160,3 @@ async def get_source_band_timeseries( timeseries=timeseries ) - -@analysis_router.get("/wo_ca/aggregate/{source_id}/{band_name}") -@requires("lcs:read") -async def get_source_band_statistics_without_continuous_aggregates( - request: Request, - source_id: int, - band_name: str, - conn: AsyncSessionDependency, - start_time: Optional[datetime] = Query( - None, - description="Start time for statistics calculation (ISO format)" - ), - end_time: Optional[datetime] = Query( - None, - description="End time for statistics calculation (ISO format)" - ), -) -> BandStatisticsResponse: - """ - Calculate statistical measures for a specific source and band using FluxMeasurementTable (Not for production) - """ - 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, _, _ = await get_band_statistics_wo_ca( - 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=start_time, - end_time=end_time, - time_resolution="daily" - ) \ No newline at end of file From 2483b34178bb822e1477057ab03268e1f0cdb522 Mon Sep 17 00:00:00 2001 From: Divesh Date: Wed, 22 Oct 2025 17:16:53 +0100 Subject: [PATCH 7/7] added timeresolution to timeseries endpoint --- lightserve/api/analysis.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lightserve/api/analysis.py b/lightserve/api/analysis.py index e7035d4..a1a6eb1 100644 --- a/lightserve/api/analysis.py +++ b/lightserve/api/analysis.py @@ -39,6 +39,7 @@ class BandTimeSeriesResponse(BaseModel): source_id: int band_name: str timeseries: BandTimeSeries + time_resolution: str @@ -146,7 +147,7 @@ async def get_source_band_timeseries( detail="start_time must be before end_time" ) - timeseries = await get_band_timeseries( + timeseries, time_resolution = await get_band_timeseries( source_id=source_id, band_name=band_name, conn=conn, @@ -157,6 +158,7 @@ async def get_source_band_timeseries( return BandTimeSeriesResponse( source_id=source_id, band_name=band_name, - timeseries=timeseries + timeseries=timeseries, + time_resolution=time_resolution )