Towards mypy cleanliness.
[kiosk.git] / weather_renderer.py
index 26c49ca8a442b39374c5106159127a0ca25c630f..fbb3ed8170f873da0a3223088b8c60462a858c1a 100644 (file)
@@ -1,38 +1,41 @@
+#!/usr/bin/env python3
+
 from datetime import datetime
-import file_writer
-import renderer
 import json
 import re
+from typing import Dict, List
+import urllib.request, urllib.error, urllib.parse
+
+import file_writer
+import renderer
 import secrets
-import urllib2
 import random
 
+
 class weather_renderer(renderer.debuggable_abstaining_renderer):
     """A renderer to fetch forecast from wunderground."""
 
-    def __init__(self,
-                 name_to_timeout_dict,
-                 file_prefix):
+    def __init__(self, name_to_timeout_dict: Dict[str, int], file_prefix: str) -> None:
         super(weather_renderer, self).__init__(name_to_timeout_dict, False)
         self.file_prefix = file_prefix
 
-    def debug_prefix(self):
-        return "weather(%s)" % (self.file_prefix)
+    def debug_prefix(self) -> str:
+        return f"weather({self.file_prefix})"
 
-    def periodic_render(self, key):
+    def periodic_render(self, key: str) -> bool:
         return self.fetch_weather()
 
-    def describe_time(self, index):
-        if (index <= 1):
+    def describe_time(self, index: int) -> str:
+        if index <= 1:
             return "overnight"
-        elif (index <= 3):
+        elif index <= 3:
             return "morning"
-        elif (index <= 5):
+        elif index <= 5:
             return "afternoon"
         else:
             return "evening"
 
-    def describe_wind(self, mph):
+    def describe_wind(self, mph: float) -> str:
         if mph <= 0.3:
             return "calm"
         elif mph <= 5.0:
@@ -44,26 +47,26 @@ class weather_renderer(renderer.debuggable_abstaining_renderer):
         else:
             return "heavy"
 
-    def describe_magnitude(self, mm):
-        if (mm < 2):
+    def describe_magnitude(self, mm: float) -> str:
+        if mm < 2.0:
             return "light"
-        elif (mm < 10):
+        elif mm < 10.0:
             return "moderate"
         else:
             return "heavy"
 
-    def describe_precip(self, rain, snow):
-        if rain == 0 and snow == 0:
+    def describe_precip(self, rain: float, snow: float) -> str:
+        if rain == 0.0 and snow == 0.0:
             return "no precipitation"
         magnitude = rain + snow
         if rain > 0 and snow > 0:
-            return "a %s mix of rain and snow" % self.describe_magnitude(magnitude)
+            return f"a {self.describe_magnitude(magnitude)} mix of rain and snow"
         elif rain > 0:
-            return "%s rain" % self.describe_magnitude(magnitude)
+            return f"{self.describe_magnitude(magnitude)} rain"
         elif snow > 0:
-            return "%s snow" % self.describe_magnitude(magnitude)
+            return f"{self.describe_magnitude(magnitude)} snow"
 
-    def fix_caps(self, s):
+    def fix_caps(self, s: str) -> str:
         r = ""
         s = s.lower()
         for x in s.split("."):
@@ -72,7 +75,9 @@ class weather_renderer(renderer.debuggable_abstaining_renderer):
         r = r.replace(". .", ".")
         return r
 
-    def pick_icon(self, conditions, rain, snow):
+    def pick_icon(
+        self, conditions: List[str], rain: List[float], snow: List[float]
+    ) -> str:
         #                     rain     snow    clouds    sun
         # fog.gif
         # hazy.gif
@@ -94,43 +99,49 @@ class weather_renderer(renderer.debuggable_abstaining_renderer):
         clear_count = 0
         total_snow = 0
         count = min(len(conditions), len(rain), len(snow))
-        for x in xrange(0, count):
-            seen_rain = rain[x] > 0;
-            seen_snow = snow[x] > 0;
+        for x in range(0, count):
+            seen_rain = rain[x] > 0
+            seen_snow = snow[x] > 0
             total_snow += snow[x]
             txt = conditions[x].lower()
-            if ("cloud" in txt):
+            if "cloud" in txt:
                 cloud_count += 1
-            if ("clear" in txt or "sun" in txt):
+            if "clear" in txt or "sun" in txt:
                 clear_count += 1
 
-        if (seen_rain and seen_snow):
-            if (total_snow < 10):
+        if seen_rain and seen_snow:
+            if total_snow < 10:
                 return "sleet.gif"
             else:
                 return "snow.gif"
-        if (seen_snow):
-            if (total_snow < 10):
+        if seen_snow:
+            if total_snow < 10:
                 return "flurries.gif"
             else:
                 return "snow.gif"
-        if (seen_rain):
+        if seen_rain:
             return "rain.gif"
-        if (cloud_count >= 6):
+        if cloud_count >= 6:
             return "mostlycloudy.gif"
-        elif (cloud_count >= 4):
+        elif cloud_count >= 4:
             return "partlycloudy.gif"
-        if (clear_count >= 7):
+        if clear_count >= 7:
             return "sunny.gif"
-        elif (clear_count >= 6):
+        elif clear_count >= 6:
             return "mostlysunny.gif"
-        elif (clear_count >= 4):
+        elif clear_count >= 4:
             return "partlysunny.gif"
         return "clear.gif"
 
-    def describe_weather(self,
-                         high, low,
-                         wind, conditions, rain, snow):
+    def describe_weather(
+        self,
+        high: float,
+        low: float,
+        wind: List[float],
+        conditions: List[str],
+        rain: List[float],
+        snow: List[float],
+    ) -> str:
         # High temp: 65
         # Low temp: 44
         #             -onight------  -morning----- -afternoon--  -evening----
@@ -148,7 +159,7 @@ class weather_renderer(renderer.debuggable_abstaining_renderer):
         lwind = ""
         lprecip = ""
         ltime = ""
-        for x in xrange(0, count):
+        for x in range(0, count):
             time = self.describe_time(x)
             current = ""
             chunks = 0
@@ -159,24 +170,24 @@ class weather_renderer(renderer.debuggable_abstaining_renderer):
             elif txt == "Rain":
                 txt = "rainy"
 
-            if (txt != lcondition):
+            if txt != lcondition:
                 if txt != "Snow" and txt != "Rain":
                     current += txt
                     chunks += 1
                 lcondition = txt
 
             txt = self.describe_wind(wind[x])
-            if (txt != lwind):
-                if (len(current) > 0):
+            if txt != lwind:
+                if len(current) > 0:
                     current += " with "
                 current += txt + " winds"
                 lwind = txt
                 chunks += 1
 
             txt = self.describe_precip(rain[x], snow[x])
-            if (txt != lprecip):
-                if (len(current) > 0):
-                    if (chunks > 1):
+            if txt != lprecip:
+                if len(current) > 0:
+                    if chunks > 1:
                         current += " and "
                     else:
                         current += " with "
@@ -184,28 +195,28 @@ class weather_renderer(renderer.debuggable_abstaining_renderer):
                 current += txt
                 lprecip = txt
 
-            if (len(current)):
-                if (ltime != time):
-                    if (random.randint(0, 3) == 0):
-                        if (time != "overnight"):
+            if len(current):
+                if ltime != time:
+                    if random.randint(0, 3) == 0:
+                        if time != "overnight":
                             descr += current + " in the " + time + ". "
                         descr += current + " overnight. "
                     else:
-                        if (time != "overnight"):
+                        if time != "overnight":
                             descr += "In the "
                         descr += time + ", " + current + ". "
                 else:
                     current = current.replace("cloudy", "clouds")
                     descr += current + " developing. "
                 ltime = time
-        if (ltime == "overnight" or ltime == "morning"):
+        if ltime == "overnight" or ltime == "morning":
             descr += "Conditions continuing the rest of the day. "
         descr = descr.replace("with breezy winds", "and breezy")
         descr = descr.replace("Clear developing", "Skies clearing")
         descr = self.fix_caps(descr)
         return descr
 
-    def fetch_weather(self):
+    def fetch_weather(self) -> None:
         if self.file_prefix == "stevens":
             text_location = "Stevens Pass, WA"
             param = "lat=47.74&lon=-121.08"
@@ -216,8 +227,10 @@ class weather_renderer(renderer.debuggable_abstaining_renderer):
             text_location = "Bellevue, WA"
             param = "id=5786882"
 
-        www = urllib2.urlopen('http://api.openweathermap.org/data/2.5/forecast?%s&APPID=%s&units=imperial' % (
-            param, secrets.openweather_key))
+        www = urllib.request.urlopen(
+            "http://api.openweathermap.org/data/2.5/forecast?%s&APPID=%s&units=imperial"
+            % (param, secrets.openweather_key)
+        )
         response = www.read()
         www.close()
         parsed_json = json.loads(response)
@@ -239,152 +252,178 @@ class weather_renderer(renderer.debuggable_abstaining_renderer):
         #     "dt_txt":"2017-01-30 18:00:00"
         #     },
         #     {"dt":1485810000,....
-        f = file_writer.file_writer('weather-%s_3_10800.html' % self.file_prefix)
-        f.write("""
-<h1>Weather at %s:</h1>
+        with file_writer.file_writer("weather-%s_3_10800.html" % self.file_prefix) as f:
+            f.write(
+                f"""
+<h1>Weather at {text_location}:</h1>
 <hr>
 <center>
-<table width=99%% cellspacing=10 border=0>
-        <tr>""" % text_location)
-        count = parsed_json['cnt']
-
-        ts = {}
-        highs = {}
-        lows = {}
-        wind = {}
-        conditions = {}
-        rain = {}
-        snow = {}
-        for x in xrange(0, count):
-            data = parsed_json['list'][x]
-            dt = data['dt_txt']  # 2019-10-07 18:00:00
-            date = dt.split(" ")[0]
-            time = dt.split(" ")[1]
-            wind[date] = []
-            conditions[date] = []
-            highs[date] = -99999
-            lows[date] = +99999
-            rain[date] = []
-            snow[date] = []
-            ts[date] = 0
-
-        for x in xrange(0, count):
-            data = parsed_json['list'][x]
-            dt = data['dt_txt']  # 2019-10-07 18:00:00
-            date = dt.split(" ")[0]
-            time = dt.split(" ")[1]
-            _ = data['dt']
-            if (_ > ts[date]):
-                ts[date] = _
-            temp = data["main"]["temp"]
-            if (highs[date] < temp):
-                highs[date] = temp
-            if (temp < lows[date]):
-                lows[date] = temp
-            wind[date].append(data["wind"]["speed"])
-            conditions[date].append(data["weather"][0]["main"])
-            if "rain" in data and "3h" in data["rain"]:
-                rain[date].append(data["rain"]["3h"])
-            else:
-                rain[date].append(0)
-            if "snow" in data and "3h" in data["snow"]:
-                snow[date].append(data["snow"]["3h"])
-            else:
-                snow[date].append(0)
-
-            # {u'clouds': {u'all': 0},
-            #  u'sys': {u'pod': u'd'},
-            #  u'dt_txt': u'2019-10-09 21:00:00',
-            #  u'weather': [
-            #      {u'main': u'Clear',
-            #       u'id': 800,
-            #       u'icon': u'01d',
-            #       u'description': u'clear sky'}
-            #  ],
-            #  u'dt': 1570654800,
-            #  u'main': {
-            #       u'temp_kf': 0,
-            #       u'temp': 54.74,
-            #       u'grnd_level': 1018.95,
-            #       u'temp_max': 54.74,
-            #       u'sea_level': 1026.46,
-            #       u'humidity': 37,
-            #       u'pressure': 1026.46,
-            #       u'temp_min': 54.74
-            #  },
-            #  u'wind': {u'speed': 6.31, u'deg': 10.09}}
-
-        # Next 5 half-days
-        #for x in xrange(0, 5):
-        #    fcast = parsed_json['forecast']['txt_forecast']['forecastday'][x]
-        #    text = fcast['fcttext']
-        #    text = re.subn(r' ([0-9]+)F', r' \1&deg;F', text)[0]
-        #    f.write('<td style="vertical-align:top;font-size:75%%"><P STYLE="padding:8px;">%s</P></td>' % text)
-        #f.write('</tr></table>')
-        #f.close()
-        #return True
-
-        #f.write("<table border=0 cellspacing=10>\n")
-        days_seen = {}
-        for date in sorted(highs.keys()):
-            today = datetime.fromtimestamp(ts[date])
-            formatted_date = today.strftime('%a %e %b')
-            if (formatted_date in days_seen):
-                continue;
-            days_seen[formatted_date] = True
-        num_days = len(days_seen.keys())
-
-        days_seen = {}
-        for date in sorted(highs.keys()):
-            precip = 0.0
-            for _ in rain[date]:
-                precip += _
-            for _ in snow[date]:
-                precip += _
-
-            today = datetime.fromtimestamp(ts[date])
-            formatted_date = today.strftime('%a %e %b')
-            if (formatted_date in days_seen):
-                continue;
-            days_seen[formatted_date] = True
-            f.write('<td width=%d%% style="vertical-align:top;">\n' % (100 / num_days))
-            f.write('<table border=0>\n')
-
-            # Date
-            f.write('  <tr><td colspan=3 height=50><b><center><font size=6>' + formatted_date + '</font></center></b></td></tr>\n')
-
-            # Icon
-            f.write('  <tr><td colspan=3 height=100><center><img src="/icons/weather/%s" height=125></center></td></tr>\n' %
-                    self.pick_icon(conditions[date], rain[date], snow[date]))
-
-            # Low temp
-            color = "#000099"
-            if (lows[date] <= 32.5):
-                color = "#009999"
-            f.write('  <tr><td width=33%% align=left><font color="%s"><b>%d&deg;F&nbsp;&nbsp;</b></font></td>\n' % (
-                color, int(lows[date])))
-
-            # Total precip
-            precip *= 0.0393701
-            if (precip > 0.025):
-                f.write('      <td width=33%%><center><b><font style="background-color:#dfdfff; color:#003355">%3.1f"</font></b></center></td>\n' % precip)
-            else:
-                f.write('      <td width=33%>&nbsp;</td>\n')
-
-            # High temp
-            color = "#800000"
-            if (highs[date] >= 80):
-                color = "#AA0000"
-            f.write('      <td align=right><font color="%s"><b>&nbsp;&nbsp;%d&deg;F</b></font></td></tr>\n' % (
-                color, int(highs[date])))
-
-            # Text "description"
-            f.write('<tr><td colspan=3 style="vertical-align:top;font-size:75%%">%s</td></tr>\n' %
-                    self.describe_weather(highs[date], lows[date], wind[date], conditions[date], rain[date], snow[date]))
-            f.write('</table>\n</td>\n')
-        f.write("</tr></table></center>")
+<table width=99% cellspacing=10 border=0>
+    <tr>"""
+            )
+            count = parsed_json["cnt"]
+
+            ts = {}
+            highs = {}
+            lows = {}
+            wind = {}
+            conditions = {}
+            rain = {}
+            snow = {}
+            for x in range(0, count):
+                data = parsed_json["list"][x]
+                dt = data["dt_txt"]  # 2019-10-07 18:00:00
+                date = dt.split(" ")[0]
+                time = dt.split(" ")[1]
+                wind[date] = []
+                conditions[date] = []
+                highs[date] = -99999
+                lows[date] = +99999
+                rain[date] = []
+                snow[date] = []
+                ts[date] = 0
+
+            for x in range(0, count):
+                data = parsed_json["list"][x]
+                dt = data["dt_txt"]  # 2019-10-07 18:00:00
+                date = dt.split(" ")[0]
+                time = dt.split(" ")[1]
+                _ = data["dt"]
+                if _ > ts[date]:
+                    ts[date] = _
+                temp = data["main"]["temp"]
+                if highs[date] < temp:
+                    highs[date] = temp
+                if temp < lows[date]:
+                    lows[date] = temp
+                wind[date].append(data["wind"]["speed"])
+                conditions[date].append(data["weather"][0]["main"])
+                if "rain" in data and "3h" in data["rain"]:
+                    rain[date].append(data["rain"]["3h"])
+                else:
+                    rain[date].append(0)
+                if "snow" in data and "3h" in data["snow"]:
+                    snow[date].append(data["snow"]["3h"])
+                else:
+                    snow[date].append(0)
+
+                # {u'clouds': {u'all': 0},
+                #  u'sys': {u'pod': u'd'},
+                #  u'dt_txt': u'2019-10-09 21:00:00',
+                #  u'weather': [
+                #      {u'main': u'Clear',
+                #       u'id': 800,
+                #       u'icon': u'01d',
+                #       u'description': u'clear sky'}
+                #  ],
+                #  u'dt': 1570654800,
+                #  u'main': {
+                #       u'temp_kf': 0,
+                #       u'temp': 54.74,
+                #       u'grnd_level': 1018.95,
+                #       u'temp_max': 54.74,
+                #       u'sea_level': 1026.46,
+                #       u'humidity': 37,
+                #       u'pressure': 1026.46,
+                #       u'temp_min': 54.74
+                #  },
+                #  u'wind': {u'speed': 6.31, u'deg': 10.09}}
+
+            # Next 5 half-days
+            # for x in xrange(0, 5):
+            #    fcast = parsed_json['forecast']['txt_forecast']['forecastday'][x]
+            #    text = fcast['fcttext']
+            #    text = re.subn(r' ([0-9]+)F', r' \1&deg;F', text)[0]
+            #    f.write('<td style="vertical-align:top;font-size:75%%"><P STYLE="padding:8px;">%s</P></td>' % text)
+            # f.write('</tr></table>')
+            # f.close()
+            # return True
+
+            # f.write("<table border=0 cellspacing=10>\n")
+            days_seen = {}
+            for date in sorted(highs.keys()):
+                today = datetime.fromtimestamp(ts[date])
+                formatted_date = today.strftime("%a %e %b")
+                if formatted_date in days_seen:
+                    continue
+                days_seen[formatted_date] = True
+            num_days = len(list(days_seen.keys()))
+
+            days_seen = {}
+            for date in sorted(highs.keys()):
+                precip = 0.0
+                for _ in rain[date]:
+                    precip += _
+                for _ in snow[date]:
+                    precip += _
+
+                today = datetime.fromtimestamp(ts[date])
+                formatted_date = today.strftime("%a %e %b")
+                if formatted_date in days_seen:
+                    continue
+                days_seen[formatted_date] = True
+                f.write(
+                    '<td width=%d%% style="vertical-align:top;">\n' % (100 / num_days)
+                )
+                f.write("<table border=0>\n")
+
+                # Date
+                f.write(
+                    "  <tr><td colspan=3 height=50><b><center><font size=6>"
+                    + formatted_date
+                    + "</font></center></b></td></tr>\n"
+                )
+
+                # Icon
+                f.write(
+                    '  <tr><td colspan=3 height=100><center><img src="/icons/weather/%s" height=125></center></td></tr>\n'
+                    % self.pick_icon(conditions[date], rain[date], snow[date])
+                )
+
+                # Low temp
+                color = "#000099"
+                if lows[date] <= 32.5:
+                    color = "#009999"
+                f.write(
+                    '  <tr><td width=33%% align=left><font color="%s"><b>%d&deg;F&nbsp;&nbsp;</b></font></td>\n'
+                    % (color, int(lows[date]))
+                )
+
+                # Total precip
+                precip *= 0.0393701
+                if precip > 0.025:
+                    f.write(
+                        '      <td width=33%%><center><b><font style="background-color:#dfdfff; color:#003355">%3.1f"</font></b></center></td>\n'
+                        % precip
+                    )
+                else:
+                    f.write("      <td width=33%>&nbsp;</td>\n")
+
+                # High temp
+                color = "#800000"
+                if highs[date] >= 80:
+                    color = "#AA0000"
+                f.write(
+                    '      <td align=right><font color="%s"><b>&nbsp;&nbsp;%d&deg;F</b></font></td></tr>\n'
+                    % (color, int(highs[date]))
+                )
+
+                # Text "description"
+                f.write(
+                    '<tr><td colspan=3 style="vertical-align:top;font-size:75%%">%s</td></tr>\n'
+                    % self.describe_weather(
+                        highs[date],
+                        lows[date],
+                        wind[date],
+                        conditions[date],
+                        rain[date],
+                        snow[date],
+                    )
+                )
+                f.write("</table>\n</td>\n")
+            f.write("</tr></table></center>")
         return True
 
-#x = weather_renderer({"Stevens": 1000},
-#                     "stevens")
-#x.periodic_render("Stevens")
+
+# x = weather_renderer({"Stevens": 1000}, "stevens")
+# x.periodic_render("Stevens")