diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | pyproject.toml | 4 | ||||
-rw-r--r-- | waterspout-radar.py | 45 | ||||
-rw-r--r-- | waterspout_radar/__main__.py | 8 | ||||
-rw-r--r-- | waterspout_radar/_config.py | 2 | ||||
-rw-r--r-- | waterspout_radar/_radar.py | 52 | ||||
-rw-r--r-- | waterspout_radar/_storage.py | 18 | ||||
-rw-r--r-- | waterspout_radar/cli.py | 19 |
8 files changed, 104 insertions, 46 deletions
@@ -3,3 +3,5 @@ __pycache__/ build*/ .venv/ .env +*.conf +*.json diff --git a/pyproject.toml b/pyproject.toml index 6faa8d8..c8118ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,10 @@ dependencies=[ dynamic=["version"] +[project.scripts] +waterspout-radar="waterspout_radar.cli:main" + + [tool.setuptools_scm] fallback_version="0" version_scheme="post-release" diff --git a/waterspout-radar.py b/waterspout-radar.py deleted file mode 100644 index ec58e64..0000000 --- a/waterspout-radar.py +++ /dev/null @@ -1,45 +0,0 @@ -import argparse -import os - -import szilagyi -from geopy.geocoders import Nominatim -from metpy import calc -from metpy.units import units -from windy import point_forecast -from windy import Windy - - -_L = point_forecast.Level - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("location") - args = parser.parse_args() - key = os.getenv("WINDY_KEY") - if not key: - raise RuntimeError("expected windy's api key") - locator = Nominatim(user_agent="waterspout-radar") - location = locator.geocode(args.location) - coords = (location.latitude, location.longitude) - registry = units - model = point_forecast.Model.ICONEU - levels = tuple(_L) - windy = Windy(registry) - forecast = windy.point_forecast(key, *coords, model, ("temp", "dewpoint", "wind", "pressure"), levels) - for pred in forecast: - dt = abs(pred.at('temp', _L.H850) - pred.at('temp', _L.SURFACE)) - pressure, _ = calc.lcl(pred["pressure"][0], pred["temp"][0], pred["dewpoint"][0]) - lcl = calc.pressure_to_height_std(pressure) - pressure, _ = calc.el(pred['pressure'], pred['temp'], pred['dewpoint']) - el = calc.pressure_to_height_std(pressure) - ccd = (el - lcl).to(units.ft) - try: - swi = szilagyi.calculate_swi(dt, ccd, pred.at('wind_u', _L.H850)) # merge with wind_v - except ValueError: - swi = -10 - print(pred.timestamp, dt, ccd, swi) - - -if __name__ == '__main__': - main() diff --git a/waterspout_radar/__main__.py b/waterspout_radar/__main__.py new file mode 100644 index 0000000..ec840b5 --- /dev/null +++ b/waterspout_radar/__main__.py @@ -0,0 +1,8 @@ +import sys + +from . import cli + +if __name__ == "__main__": + if "__main__" in sys.arg[0]: + sys.argv[0] = "waterspout_radar" + cli.main() diff --git a/waterspout_radar/_config.py b/waterspout_radar/_config.py index 36d9c0b..eaa6c58 100644 --- a/waterspout_radar/_config.py +++ b/waterspout_radar/_config.py @@ -39,7 +39,7 @@ class ConfigError(Exception): "Raised when configuration could not be loaded or was malformed." -def load(pathname: str="radar.conf") -> Config: +def load(pathname: str) -> Config: "Loads configuration from a file at *pathname*. May raise ConfigError when content does not meet expectations." config = configparser.ConfigParser(allow_no_value=True) config.read(pathname) diff --git a/waterspout_radar/_radar.py b/waterspout_radar/_radar.py new file mode 100644 index 0000000..629ec55 --- /dev/null +++ b/waterspout_radar/_radar.py @@ -0,0 +1,52 @@ +import dataclasses +import datetime +import typing + +import szilagyi +from geopy import geocoders +from metpy import calc +from metpy import units as metunits +from windy import point_forecast, Windy + +_L = point_forecast.Level + + +@dataclasses.dataclass +class Prediction: + time: datetime.datetime + swi: float + + +def calculate(config) -> typing.List[Prediction]: + units = metunits.units + windy = Windy(units) + + def _calculate(latitude, longitude): + forecasts = windy.point_forecast( + config.key, + latitude, longitude, + point_forecast.Model.ICONEU, + ("temp", "dewpoint", "wind", "pressure"), + tuple(_L)) + for cast in forecasts: + dt = abs(cast.at("temp", _L.H850) - cast.at("temp", _L.SURFACE)) + pressure, _ = calc.lcl( + cast.at("pressure", _L.SURFACE), + cast.at("temp", _L.SURFACE), + cast.at("dewpoint", _L.SURFACE)) + lcl = calc.pressure_to_height_std(pressure) + pressure, _ = calc.el(cast["pressure"], cast["temp"], cast["dewpoint"]) + el = calc.pressure_to_height_std(pressure) + ccd = (el - lcl).to(units.ft) + try: + swi = szilagyi.calculate_swi(dt, ccd) + except ValueError: + swi = -10 + yield Prediction(time=cast.timestamp, swi=swi) + + predictions = [] + locator = geocoders.Nominatim(user_agent="waterspout-radar") + for location in config.locations: + found = locator.geocode(location) + predictions.extend(_calculate(found.latitude, found.longitude)) + return predictions diff --git a/waterspout_radar/_storage.py b/waterspout_radar/_storage.py new file mode 100644 index 0000000..a410259 --- /dev/null +++ b/waterspout_radar/_storage.py @@ -0,0 +1,18 @@ +import datetime +import typing + +from . import _radar + +Period = typing.Tuple[datetime.datetime, datetime.datetime] + + +class Storage: + def __init__(self, pathname): + pass + + def show(self, period: Period=None): + return [] + + def extend(self, predictions: typing.Iterable[_radar.Prediction]): + for prediction in predictions: + print(prediction) diff --git a/waterspout_radar/cli.py b/waterspout_radar/cli.py new file mode 100644 index 0000000..ecf79d8 --- /dev/null +++ b/waterspout_radar/cli.py @@ -0,0 +1,19 @@ +import argparse + +from . import _config, _radar, _storage + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "-c", "--config", default="radar.conf", + help="Pathname of the instance configuration file (default: %(default)s)") + args = parser.parse_args() + config = _config.load(args.config) + storage = _storage.Storage(config.db) + predictions = _radar.calculate(config) + storage.extend(predictions) + + +if __name__ == "__main__": + main() |