about summary refs log tree commit diff
diff options
context:
space:
mode:
authorStarfall <us@starfall.systems>2022-12-01 13:52:42 -0600
committerStarfall <us@starfall.systems>2022-12-01 13:52:42 -0600
commit1416ec325ac9a35aaa50cd1e7a77cd6112a8e5e0 (patch)
tree4d090c6f6fb9cbc0a9d1fe0fbd4720b86b4b6b9d
parentd528ba125fae31e3caa76471136fa0a821867884 (diff)
cache forecasts on filesystem
-rw-r--r--.gitignore1
-rw-r--r--fscache.py32
-rwxr-xr-xmeteor.py73
3 files changed, 73 insertions, 33 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..bee8a64
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+__pycache__
diff --git a/fscache.py b/fscache.py
new file mode 100644
index 0000000..d52ff62
--- /dev/null
+++ b/fscache.py
@@ -0,0 +1,32 @@
+import os
+import time
+from pathlib import Path
+
+class CacheMiss(Exception):
+    pass
+
+class Cache:
+    def __init__(self, name, ttl=3600):
+        self.ttl = ttl
+        try:
+            self.cache_dir = Path(os.environ['XDG_CACHE_HOME'])/name
+        except KeyError:
+            self.cache_dir = Path(os.environ['HOME'])/'.cache'/name
+        self.cache_dir.mkdir(exist_ok=True)
+
+    def write(self, key, value):
+        # TODO gracefully handle keys with '/' by creating directories or rewriting
+        loc = self.cache_dir/key
+        loc.write_text(value, errors='ignore')
+
+    def read(self, key):
+        loc = self.cache_dir/key
+
+        if not loc.exists():
+            raise CacheMiss
+
+        age = time.time() - loc.stat().st_mtime
+        if age > self.ttl:
+            raise CacheMiss
+        
+        return loc.read_text()
diff --git a/meteor.py b/meteor.py
index f19a834..fb3179c 100755
--- a/meteor.py
+++ b/meteor.py
@@ -1,6 +1,7 @@
 #!/usr/bin/env python3
 from argparse import ArgumentParser
 import requests
+import fscache as fs
 
 # doco is at https://www.weather.gov/documentation/services-web-api
 # openAPI spec is at https://api.weather.gov/openapi.json
@@ -53,35 +54,49 @@ def print_alerts(county, zone):
         # can construct the headline ourseves as f'{event} issued {sent} [until {end}] by {senderName}'
     print()
 
-def print_forecast(forecast_url):
+def print_forecast(point):
     # TODO pick up gridId AFC (AER/ALU), AFG, AJK, BOX< CAE, DLH, FSD, HGX, HNX, LIX, LWX, MAF, MFR, MLB, MRX, MTR, PIH and reduce gridX and gridY by 1 - known issue
 
-    forecast = requests.get(forecast_url, headers=headers).json()
-    # TODO retry on 500 - known issue
-    # TODO query param units=si
-    # TODO cache for like four hours per location, forecast doesn't change that frequently
-
-    # TODO replace the whole thing with /gridpoints/{point.gridId}/{gridX},{gridY} and create our own forecast
-    # there is lots more information there that would be useful
-    # probabilityOfPrecipitation is the neatest one that's not in .../forecast or .../forecast/hourly
-    # but there's also relativeHumidity, apparentTemperature (and, separately, windChill), skyCover, windGust
-    # downside, it appears to always convert the F/mph measurements into C/kph for no reason. e.g. all temps are measured in 9ths of C
-
-    print(f'{term.BOLD}forecast')
-    print(f'===================={term.RESET}')
-    periods = forecast.get('periods')
-    # print today and tonight in long format
-    for period in periods[:2]:
-        print(f'{term.BOLD}{period.get("name")}{term.RESET}:')
-        print(f'  {period.get("temperature")}°{period.get("temperatureUnit")}, wind {period.get("windSpeed")} {period.get("windDirection")}')
-        print(f'  {period.get("detailedForecast")}')
-    # print further forecasted periods in short format
-    for period in periods[2:]:
-        # TODO parse isDaytime true/false to squish a day together
-        print(f'{term.BOLD}{period.get("name").ljust(15)}{term.RESET}: {period.get("shortForecast")} - {period.get("temperature")}°{period.get("temperatureUnit")}, {period.get("windSpeed")} {period.get("windDirection")}')
+    cache_key = f'{point.get("gridId")}-{point.get("gridX")}-{point.get("gridY")}'
+
+    try: 
+        print(cache.read(cache_key))
+    except fs.CacheMiss:
+        forecast_url = point.get('forecast')
+        forecast = requests.get(forecast_url, headers=headers).json()
+        # TODO retry on 500 - known issue
+        # TODO query param units=si
+
+        # TODO replace the whole thing with /gridpoints/{point.gridId}/{gridX},{gridY} and create our own forecast
+        # there is lots more information there that would be useful
+        # probabilityOfPrecipitation is the neatest one that's not in .../forecast or .../forecast/hourly
+        # but there's also relativeHumidity, apparentTemperature (and, separately, windChill), skyCover, windGust
+        # downside, it appears to always convert the F/mph measurements into C/kph for no reason. e.g. all temps are measured in 9ths of C
+    
+        # TODO pick out location name from somewhere.
+        # city and state hide in point.relativeLocation.properties.city (and state)
+        # getting the county or zone's `name (id) state` is good too
+
+        buffer = f'{term.BOLD}forecast'
+        buffer += '\n' + f'===================={term.RESET}'
+        periods = forecast.get('periods')
+        # print today and tonight in long format
+        for period in periods[:2]:
+            buffer += '\n' + f'{term.BOLD}{period.get("name")}{term.RESET}:'
+            buffer += '\n' + f'  {period.get("temperature")}°{period.get("temperatureUnit")}, wind {period.get("windSpeed")} {period.get("windDirection")}'
+            buffer += '\n' + f'  {period.get("detailedForecast")}'
+        # print further forecasted periods in short format
+        for period in periods[2:]:
+            # TODO parse isDaytime true/false to squish a day together
+            buffer += '\n' + f'{term.BOLD}{period.get("name").ljust(15)}{term.RESET}: {period.get("shortForecast")} - {period.get("temperature")}°{period.get("temperatureUnit")}, {period.get("windSpeed")} {period.get("windDirection")}'
+
+        cache.write(cache_key, buffer)
+        print(buffer)
 
 def main():
     args = handle_args()
+    global cache
+    cache = fs.Cache('meteor.py')
 
     point = requests.get(base_url + f'/points/{args.lat},{args.lon}',
                          headers=headers).json()
@@ -90,17 +105,9 @@ def main():
     zone = point.get('forecastZone').split('/')[-1]
     print_alerts(county, zone)
 
-    # TODO pick out location name from somewhere.
-    # city and state hide in point.relativeLocation.properties.city (and state)
-    # getting the county or zone's `name (id) state` is good too
-
     # TODO can hazardous weather outlook be gotten programatically?
 
-
-    forecast_url = point.get('forecast')
-    print_forecast(forecast_url)
-
-    # hourly_forecast_url = point.get('forecastHourly')
+    print_forecast(point)
 
 if __name__ == '__main__':
     import sys