Skip to content

Commit

Permalink
✨ Migrate climate normals dl backend to python
Browse files Browse the repository at this point in the history
  • Loading branch information
akrherz committed Jan 28, 2025
1 parent c2dd722 commit fd87606
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 100 deletions.
3 changes: 3 additions & 0 deletions cgi-bin/request/normals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""implemented in /pylib/iemweb/request/normals.py"""

from iemweb.request.normals import application # noqa: F401
19 changes: 10 additions & 9 deletions htdocs/COOP/dl/normals.phtml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ $dselect = daySelect("day");

$sselect = selectClimodatNetwork($network, "network");

$t->content = <<<EOF
$t->content = <<<EOM
<p>With this interface, you can download daily climate normals for NWS COOP
sites. Please fill out the form below:</p>
Expand All @@ -27,13 +27,14 @@ sites. Please fill out the form below:</p>
<input type="submit" value="Select State">
</form>
<form method="GET" action="normals_wkr.php" name="dl">
<form method="GET" action="/cgi-bin/request/normals.py" name="dl">
<input type="hidden" name="network" value="{$network}">
<h3>1. Climatology Source:</h3>
<blockquote>
(<i>Based on daily observations, the IEM has computed standard climatological values. The
official data is the 30 year record from NCDC.</i>)
</blockquote>
<p>The IEM maintains a set of station idenitifers that does not exactly match
what NCEI uses. When you select the NCEI climatology and an IEM station identifier,
you are getting the cross reference between the two idenitifer sets. Hopefully, this
is generally one to one, but it could be "nearest station"</p>
<strong>Select Data Source</strong><br />
<select name="source">
Expand All @@ -54,13 +55,13 @@ official data is the 30 year record from NCDC.</i>)
<input type="radio" name="mode" value="station" checked="CHECKED">Single Station, All Days</input>
<br>{$nselect}
</div>
<div class="col-md-6">
<p><b>2b. Select Month & Date:</b><br>
<input type="radio" name="mode" value="day">All Stations, One Day</input>
<br />Select Month: {$mselect}
<br />Select Day: {$dselect}
</div>
Expand All @@ -84,5 +85,5 @@ open the comma delimited file.
</form><p>
<p><img src="/images/gisready.png"> Data includes Lat/Lon information.
EOF;
EOM;
$t->render('single.phtml');
91 changes: 0 additions & 91 deletions htdocs/COOP/dl/normals_wkr.php

This file was deleted.

1 change: 1 addition & 0 deletions htdocs/api/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@
<li><a href="/cgi-bin/request/hml.py?help">HML Processed Data (/cgi-bin/request/hml.py)</a></li>
<li><a href="/cgi-bin/request/hourlyprecip.py?help">Hourly Precip (/cgi-bin/request/hourlyprecip.py)</a></li>
<li><a href="/cgi-bin/request/nass_iowa.py?help">Iowa NASS (/cgi-bin/request/nass_iowa.py)</a></li>
<li><a href="/cgi-bin/request/normals.py?help">NCEI/IEM Climate Normals (/cgi-bin/request/normals.py)</a></li>
<li><a href="/cgi-bin/request/isusm.py?help">Iowa State Soil Moisture Network (/cgi-bin/request/isusm.py)</a></li>
<li><a href="/cgi-bin/request/coop.py?help">IEM Climodat stations (/cgi-bin/request/coop.py)</a></li>
<li><a href="/cgi-bin/request/daily.py?help">IEM Computed Daily Summaries (/cgi-bin/request/daily.py)</a></li>
Expand Down
143 changes: 143 additions & 0 deletions pylib/iemweb/request/normals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
""".. title:: Climatology Data
Return to `API Services </api/#cgi>`_ or the
`User Frontend </COOP/dl/normals.phml>`_.
This service emits the daily climatology, sometimes referred to as normals,
but normals is a poor name, but I digress. You can either request entire
state's climatology values for a specific day or a single locations climatology
for the entire year. Day values use the year 2000, but only imply the day
of the year.
Changelog
---------
- 2025-01-28: Initial implementation
Example Requests
----------------
Provide the January 1 climatology for all stations in Iowa using the NCEI
1991-2020 dataset.
https://mesonet.agron.iastate.edu/cgi-bin/request/normals.py\
?mode=day&month=1&day=1&source=ncei_climate91
Provide the IEM computed period of record climatology for Ames, IA in Excel
https://mesonet.agron.iastate.edu/cgi-bin/request/normals.py\
?mode=station&station=IATAME&source=climate&fmt=excel
Provide the NCEI 1981-2010 climatology for Ames, IA in JSON, this has a source
of ncdc_climate81 due to lame reasons.
https://mesonet.agron.iastate.edu/cgi-bin/request/normals.py\
?mode=station&station=IATAME&source=ncdc_climate81&fmt=json
"""

from datetime import date
from io import BytesIO

import pandas as pd
from pydantic import Field
from pyiem.database import get_sqlalchemy_conn
from pyiem.network import Table as NetworkTable
from pyiem.webutil import CGIModel, iemapp
from sqlalchemy import text

EXL = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"


class Schema(CGIModel):
"""See how we are called."""

day: int = Field(
default=1,
description="The day of the year, only used for day mode",
ge=1,
le=31,
)
fmt: str = Field(
default="csv",
description="The format of the output, either csv, json, excel",
pattern="^(csv|json|excel)$",
)
mode: str = Field(
default="station",
description="The mode of request, either station or day",
pattern="^(station|day)$",
)
month: int = Field(
default=1,
description="The month of the year, only used for day mode",
ge=1,
le=12,
)
source: str = Field(
default="ncei_climate91",
description="The source of the data, defaults to ncei_climate91",
pattern=r"^(climate\d?\d?|ncdc_climate\d?\d?|ncei_climate\d?\d?)$",
)
station: str = Field(
default="IA0000",
description="The station identifier, only used for station mode",
)


@iemapp(help=__doc__, schema=Schema)
def application(environ, start_response):
"""Go Main Go"""
source = environ["source"]
st = environ["station"][:2].upper()
network = f"{st}CLIMATE"
nt = NetworkTable(network, only_online=False)
params = {
"station": environ["station"],
"day": date(2000, environ["month"], environ["day"]),
"stations": list(nt.sts.keys()),
"network": network,
}
if environ["mode"] == "station":
limiter = "station = :station"
elif environ["mode"] == "day":
limiter = "valid = :day and station = ANY(:stations)"
if source.startswith("ncei"):
col = "ncdc81" if source == "ncei_climate81" else "ncei91"
params["network"] = col.upper()
if environ["mode"] == "station":
with get_sqlalchemy_conn("mesosite") as conn:
res = conn.execute(
text(
f"select {col} as station from stations "
"where id = :station and network ~* 'CLIMATE'"
),
{"station": environ["station"]},
)
if res.rowcount > 0:
row = res.fetchone()
params["station"] = row[0]
else:
# Woof
params["stations"] = [meta[col] for meta in nt.sts.values()]

with get_sqlalchemy_conn("coop") as pgconn:
climodf = pd.read_sql(
text(f"""
select c.*, st_x(geom) as lon, st_y(geom) as lat, name
from {source} c, stations t WHERE c.station = t.id
and t.network = :network and {limiter}
"""),
pgconn,
params=params,
)
if environ["fmt"] == "json":
start_response("200 OK", [("Content-type", "application/json")])
return climodf.to_json(orient="records")
if environ["fmt"] == "excel":
start_response("200 OK", [("Content-type", EXL)])
bio = BytesIO()
climodf.to_excel(bio, index=False)
return bio.getvalue()
start_response("200 OK", [("Content-type", "text/plain")])
return climodf.to_csv(index=False)

0 comments on commit fd87606

Please sign in to comment.