Skip to content

Commit

Permalink
Adding flags endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
caparker committed Oct 14, 2024
1 parent ecc6859 commit 7ec6e73
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 5 deletions.
2 changes: 2 additions & 0 deletions openaq_api/openaq_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
tiles,
licenses,
latest,
flags,
)

logging.basicConfig(
Expand Down Expand Up @@ -228,6 +229,7 @@ def favico():
app.include_router(providers.router)
app.include_router(sensors.router)
app.include_router(latest.router)
app.include_router(flags.router)


# app.include_router(auth_router)
Expand Down
2 changes: 2 additions & 0 deletions openaq_api/openaq_api/v3/models/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,8 @@ def where(self) -> str:
return f"{dt} <= :date_to::date"




class PeriodNames(StrEnum):
hour = "hour"
day = "day"
Expand Down
20 changes: 20 additions & 0 deletions openaq_api/openaq_api/v3/models/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,22 @@ class Location(JsonBase):
datetime_last: DatetimeObject | None = None


class Flag(JsonBase):
id: int = Field(alias='flags_id')
label: str
level: str
invalidates: bool

class LocationFlag(JsonBase):
#model_config = ConfigDict(exclude_unset=True)
location_id: int
flag: Flag
datetime_from: DatetimeObject
datetime_to: DatetimeObject
sensor_ids: list[int] = []
note: str | None = None


class Measurement(JsonBase):
value: float
parameter: ParameterBase
Expand Down Expand Up @@ -334,6 +350,10 @@ class SensorsResponse(OpenAQResult):
results: list[Sensor]


class LocationFlagsResponse(OpenAQResult):
results: list[LocationFlag]


class ProvidersResponse(OpenAQResult):
results: list[Provider]

Expand Down
144 changes: 144 additions & 0 deletions openaq_api/openaq_api/v3/routers/flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import logging
from typing import Annotated, Any
from datetime import datetime, date

from fastapi import APIRouter, Depends, Path, Query
from fastapi.exceptions import RequestValidationError

from pydantic import model_validator

from openaq_api.db import DB
from openaq_api.v3.models.queries import (
Paging,
#DatetimeFromQuery,
#DatetimeToQuery,
QueryBaseModel,
QueryBuilder,
)

from openaq_api.v3.models.responses import (
LocationFlagsResponse,
)

logger = logging.getLogger("flags")

router = APIRouter(
prefix="/v3",
tags=["v3"],
include_in_schema=True,
)

class DatetimePeriodQuery(QueryBaseModel):
datetime_from: datetime | date | None = Query(
None,
description="To when?",
examples=["2022-10-01T11:19:38-06:00", "2022-10-01"],
)
datetime_to: datetime | date | None = Query(
None,
description="To when?",
examples=["2022-10-01T11:19:38-06:00", "2022-10-01"],
)

@model_validator(mode="after")
@classmethod
def check_dates_are_in_order(cls, data: Any) -> Any:
dt = getattr(data, "datetime_to")
df = getattr(data, "datetime_from")
if dt and df and dt <= df:
raise RequestValidationError(
f"Date/time from must be older than the date/time to. User passed {df} - {dt}"
)

def where(self) -> str:
pd = self.map("period", "period")
if self.datetime_to is None and self.datetime_from is None:
return None
if self.datetime_to is not None and self.datetime_from is not None:
return f"{pd} && tstzrange(:datetime_from, :datetime_to, '[]')"
elif self.datetime_to is not None:
return f"{pd} && tstzrange('-infinity'::timestamptz, :datetime_to, '[]')"
elif self.datetime_from is not None:
return f"{pd} && tstzrange(:datetime_from, 'infinity'::timestamptz, '[]')"





class LocationFlagQuery(QueryBaseModel):
locations_id: int = Path(
..., description="Limit the results to a specific locations", ge=1
)

def where(self):
return "f.sensor_nodes_id = :locations_id"


class SensorFlagQuery(QueryBaseModel):
sensor_id: int = Path(
..., description="Limit the results to a specific sensor", ge=1
)

def where(self):
return "ARRAY[:sensor_id::int] @> fm.sensors_ids"


class LocationFlagQueries(LocationFlagQuery, DatetimePeriodQuery, Paging):
...


class SensorFlagQueries(SensorFlagQuery, DatetimePeriodQuery, Paging):
...



@router.get(
"/locations/{locations_id}/flags",
response_model=LocationFlagsResponse,
summary="Get flags by location ID",
description="Provides a list of flags by location ID",
)
async def location_flags_get(
location_flags: Annotated[
LocationFlagQueries, Depends(LocationFlagQueries.depends())
],
db: DB = Depends(),
):
return await fetch_flags(location_flags, db)


@router.get(
"/sensors/{sensor_id}/flags",
response_model=LocationFlagsResponse,
summary="Get flags by sensor ID",
description="Provides a list of flags by sensor ID",
)
async def sensor_flags_get(
sensor_flags: Annotated[
SensorFlagQueries, Depends(SensorFlagQueries.depends())
],
db: DB = Depends(),
):
return await fetch_flags(sensor_flags, db)



async def fetch_flags(q, db):
query = QueryBuilder(q)
query.set_column_map({"timezone": "tz.tzid", "datetime": "lower(period)"})

sql = f"""
SELECT fm.sensor_nodes_id as location_id
, json_build_object('id', f.flags_id, 'label', f.label, 'level', fl.label, 'invalidates', fl.invalidates) as flag
, sensors_ids
, get_datetime_object(lower(fm.period), t.tzid) as datetime_from
, get_datetime_object(upper(fm.period), t.tzid) as datetime_to
, note
FROM flagged_measurements fm
JOIN flags f ON (fm.flags_id = f.flags_id)
JOIN flag_levels fl ON (f.flag_levels_id = fl.flag_levels_id)
JOIN sensor_nodes n ON (fm.sensor_nodes_id = n.sensor_nodes_id)
JOIN timezones t ON (n.timezones_id = t.timezones_id)
{query.where()}
"""
return await db.fetchPage(sql, query.params())
10 changes: 5 additions & 5 deletions openaq_api/openaq_api/v3/routers/sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,17 @@ async def fetch_sensors(q, db):
, 'display_name', m.display
) as parameter
, s.sensors_id
, CASE
, CASE
WHEN r.value_latest IS NOT NULL THEN
json_build_object(
'min', r.value_min
, 'max', r.value_max
, 'avg', r.value_avg
, 'sd', r.value_sd
)
)
ELSE NULL
END as summary
, CASE
, CASE
WHEN r.value_latest IS NOT NULL THEN
jsonb_build_object(
'datetime_from', get_datetime_object(r.datetime_first, t.tzid),
Expand All @@ -106,15 +106,15 @@ async def fetch_sensors(q, db):
END as coverage
, get_datetime_object(r.datetime_first, t.tzid) as datetime_first
, get_datetime_object(r.datetime_last, t.tzid) as datetime_last
,CASE
,CASE
WHEN r.value_latest IS NOT NULL THEN
json_build_object(
'datetime', get_datetime_object(r.datetime_last, t.tzid)
, 'value', r.value_latest
, 'coordinates', json_build_object(
'latitude', st_y(COALESCE(r.geom_latest, n.geom))
,'longitude', st_x(COALESCE(r.geom_latest, n.geom))
))
))
ELSE NULL
END as latest
FROM sensors s
Expand Down

0 comments on commit 7ec6e73

Please sign in to comment.