Skip to content

Commit

Permalink
Add climatology endpoint and viz
Browse files Browse the repository at this point in the history
  • Loading branch information
mfisher87 committed Jul 3, 2024
1 parent c7be8fb commit 274365e
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 13 deletions.
4 changes: 4 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ repos:
rev: "v2.2.6"
hooks:
- id: codespell
exclude: >
(?x)^(
.*\.ipynb
)$
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: "v0.10.0.1"
Expand Down
92 changes: 80 additions & 12 deletions demo.ipynb

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions src/aross_stations_db/api/v1/climatology.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import datetime as dt
from typing import Annotated

from fastapi import APIRouter, Depends, Query
from geoalchemy2 import WKTElement
from sqlalchemy.orm import Session

from aross_stations_db.api.v1.output import (
ClimatologyJsonElement,
climatology_query_results_to_json,
)
from aross_stations_db.middleware import get_db_session
from aross_stations_db.query import climatology_query

router = APIRouter()


@router.get("/monthly")
def get_monthly_climatology(
db: Annotated[Session, Depends(get_db_session)],
*,
start: Annotated[dt.datetime, Query(description="ISO-format timestamp")],
end: Annotated[dt.datetime, Query(description="ISO-format timestamp")],
polygon: Annotated[str | None, WKTElement, Query(description="WKT shape")] = None,
) -> list[ClimatologyJsonElement]:
"""Get a monthly climatology of events matching query parameters."""
# TODO: Validate query spans >1 year? Or >= 2 years? Should the query parameters be
# changed to enforce best practices for visualizing a climatology (e.g. there
# should always be the same number of Januaries as Februaries as ...etc., so we
# could accept a start year, end year, and start month.)
query = climatology_query(db=db, start=start, end=end, polygon=polygon)

return climatology_query_results_to_json(query.all())
16 changes: 16 additions & 0 deletions src/aross_stations_db/api/v1/output.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import datetime as dt
from typing import Annotated

from annotated_types import Ge, Le
from geojson_pydantic import (
Feature,
FeatureCollection,
Expand Down Expand Up @@ -48,3 +50,17 @@ def timeseries_query_results_to_json(
TimeseriesJsonElement(date=date, event_count=event_count)
for date, event_count in results
]


class ClimatologyJsonElement(BaseModel):
month: Annotated[int, Ge(1), Le(12)]
event_count: int


def climatology_query_results_to_json(
results: list[Row],
) -> list[ClimatologyJsonElement]:
return [
ClimatologyJsonElement(month=month, event_count=event_count)
for month, event_count in results
]
3 changes: 2 additions & 1 deletion src/aross_stations_db/api/v1/routes.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from fastapi import APIRouter

from aross_stations_db.api.v1.climatology import router as climatology_router
from aross_stations_db.api.v1.stations import router as stations_router
from aross_stations_db.api.v1.timeseries import router as timeseries_router

router = APIRouter()
router.include_router(stations_router, prefix="/stations")
router.include_router(timeseries_router, prefix="/events/timeseries")
# router.include_router(climatology_router, prefix="/events/climatology")
router.include_router(climatology_router, prefix="/events/climatology")
30 changes: 30 additions & 0 deletions src/aross_stations_db/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,33 @@ def timeseries_query(
)

return query.group_by("month").order_by("month")


def climatology_query(
db: Session,
*,
start: dt.datetime,
end: dt.datetime,
polygon: str | None = None,
) -> RowReturningQuery[tuple[int, int]]:
query = cast(
# TODO: A better way! Avoid duplicating information; the type of func.count()
# can be inferred automatically, but not func.date_trunc().
# https://github.com/sqlalchemy/sqlalchemy/discussions/11564
RowReturningQuery[tuple[int, int]],
db.query(
func.extract("month", Event.time_start).label("month"),
func.count(Event.time_start).label("count"),
),
).filter(Event.time_start >= start, Event.time_end < end)

if polygon:
query = query.join(
Station, # TODO: Event.station relationship
).filter(
Station.location.ST_Within(
func.ST_SetSRID(func.ST_GeomFromText(polygon), 4326),
)
)

return query.group_by("month").order_by("month")

0 comments on commit 274365e

Please sign in to comment.