Tweak around docstring to make prettier sphinx autodocs.
[python_utils.git] / cached / weather_data.py
index 8d49736bae0cf264fc7447c150011f616ccec388..87c3260c0a5b90078f567f3a94bfcac8f03d5ea5 100644 (file)
@@ -1,18 +1,27 @@
 #!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# © Copyright 2021-2022, Scott Gasch
+
+"""How's the weather?"""
 
-from dataclasses import dataclass
 import datetime
 import json
 import logging
 import os
-from typing import List
 import urllib.request
+from dataclasses import dataclass
+from typing import Any, Dict, List, Optional
+
+from overrides import overrides
 
 import argparse_utils
 import config
 import datetime_utils
 import list_utils
 import persistent
+import scott_secrets
+import type_utils
 
 logger = logging.getLogger(__name__)
 
@@ -23,34 +32,40 @@ cfg = config.add_commandline_args(
 cfg.add_argument(
     '--weather_data_cachefile',
     type=str,
-    default=f'{os.environ["HOME"]}/.weather_summary_cache',
+    default=f'{os.environ["HOME"]}/cache/.weather_summary_cache',
     metavar='FILENAME',
-    help='File in which to cache weather data'
+    help='File in which to cache weather data',
 )
 cfg.add_argument(
     '--weather_data_stalest_acceptable',
     type=argparse_utils.valid_duration,
-    default=datetime.timedelta(seconds=7200),   # 2 hours
+    default=datetime.timedelta(seconds=7200),  # 2 hours
     metavar='DURATION',
-    help='Maximum acceptable age of cached data.  If zero, forces a refetch'
+    help='Maximum acceptable age of cached data.  If zero, forces a refetch',
 )
 
 
 @dataclass
 class WeatherData:
-    date: datetime.date              # The date
-    high: float                      # The predicted high in F
-    low: float                       # The predicted low in F
-    precipitation_inches: float      # Number of inches of precipitation / day
-    conditions: List[str]            # Conditions per ~3h window
-    most_common_condition: str       # The most common condition
-    icon: str                        # An icon to represent it
+    date: datetime.date  # The date
+    high: float  # The predicted high in F
+    low: float  # The predicted low in F
+    precipitation_inches: float  # Number of inches of precipitation / day
+    conditions: List[str]  # Conditions per ~3h window
+    most_common_condition: str  # The most common condition
+    icon: str  # An icon to represent it
 
 
[email protected]_autoloaded_singleton()
[email protected]_autoloaded_singleton()  # type: ignore
 class CachedWeatherData(persistent.Persistent):
-    def __init__(self,
-                 weather_data = None):
+    def __init__(self, weather_data: Dict[datetime.date, WeatherData] = None):
+        """C'tor.  Do not pass a dict except for testing purposes.
+
+        The @persistent_autoloaded_singleton decorator handles
+        invoking our load and save methods at construction time for
+        you.
+        """
+
         if weather_data is not None:
             self.weather_data = weather_data
             return
@@ -70,16 +85,16 @@ class CachedWeatherData(persistent.Persistent):
             "Sand": "🏜️",
             "Ash": "🌋",
             "Squall": "🌬",
-            "Tornado": "🌪️"
+            "Tornado": "🌪️",
         }
         now = datetime.datetime.now()
         dates = set()
-        highs = {}
-        lows = {}
-        conditions = {}
-        precip = {}
-        param = "id=5786882"   # Bellevue, WA
-        key = "c0b160c49743622f62a9cd3cda0270b3"
+        highs: Dict[datetime.date, Optional[float]] = {}
+        lows: Dict[datetime.date, Optional[float]] = {}
+        conditions: Dict[datetime.date, List[str]] = {}
+        precip: Dict[datetime.date, float] = {}
+        param = "id=5786882"  # Bellevue, WA
+        key = scott_secrets.OPEN_WEATHER_MAP_KEY
         www = urllib.request.urlopen(
             f'http://api.openweathermap.org/data/2.5/weather?zip=98005,us&APPID={key}&units=imperial'
         )
@@ -105,13 +120,13 @@ class CachedWeatherData(persistent.Persistent):
         if dt == now.date() and now.hour > 18 and condition == 'Clear':
             icon = '🌙'
         self.weather_data[dt] = WeatherData(
-            date = dt,
-            high = float(parsed_json["main"]["temp_max"]),
-            low = float(parsed_json["main"]["temp_min"]),
-            precipitation_inches = p / 25.4,
-            conditions = [condition],
-            most_common_condition = condition,
-            icon = icon,
+            date=dt,
+            high=float(parsed_json["main"]["temp_max"]),
+            low=float(parsed_json["main"]["temp_min"]),
+            precipitation_inches=p / 25.4,
+            conditions=[condition],
+            most_common_condition=condition,
+            icon=icon,
         )
 
         www = urllib.request.urlopen(
@@ -132,9 +147,9 @@ class CachedWeatherData(persistent.Persistent):
                 lows[dt] = None
                 conditions[dt] = []
             for temp in (
-                    data["main"]["temp"],
-                    data['main']['temp_min'],
-                    data['main']['temp_max'],
+                data["main"]["temp"],
+                data['main']['temp_min'],
+                data['main']['temp_max'],
             ):
                 if highs[dt] is None or temp > highs[dt]:
                     highs[dt] = temp
@@ -158,43 +173,59 @@ class CachedWeatherData(persistent.Persistent):
         for dt in sorted(dates):
             if dt == today:
                 high = highs.get(dt, None)
-                if (
-                        high is not None and
-                        self.weather_data[today].high < high
-                ):
+                if high is not None and self.weather_data[today].high < high:
                     self.weather_data[today].high = high
                 continue
-            most_common_condition = list_utils.most_common_item(conditions[dt])
+            most_common_condition = list_utils.most_common(conditions[dt])
             icon = icon_by_condition.get(most_common_condition, '?')
             if dt == now.date() and now.hour > 18 and condition == 'Clear':
                 icon = '🌙'
             self.weather_data[dt] = WeatherData(
-                date = dt,
-                high = highs[dt],
-                low = lows[dt],
-                precipitation_inches = precip[dt] / 25.4,
-                conditions = conditions[dt],
-                most_common_condition = most_common_condition,
-                icon = icon
+                date=dt,
+                high=type_utils.unwrap_optional(highs[dt]),
+                low=type_utils.unwrap_optional(lows[dt]),
+                precipitation_inches=precip[dt] / 25.4,
+                conditions=conditions[dt],
+                most_common_condition=most_common_condition,
+                icon=icon,
             )
 
     @classmethod
-    def load(cls):
+    @overrides
+    def load(cls) -> Any:
+
+        """Depending on whether we have fresh data persisted either uses that
+        data to instantiate the class or makes an HTTP request to fetch the
+        necessary data.
+
+        Note that because this is a subclass of Persistent this is taken
+        care of automatically.
+        """
+
         if persistent.was_file_written_within_n_seconds(
-                config.config['weather_data_cachefile'],
-                config.config['weather_data_stalest_acceptable'].total_seconds(),
+            config.config['weather_data_cachefile'],
+            config.config['weather_data_stalest_acceptable'].total_seconds(),
         ):
             import pickle
+
             with open(config.config['weather_data_cachefile'], 'rb') as rf:
                 weather_data = pickle.load(rf)
                 return cls(weather_data)
         return None
 
-    def save(self):
+    @overrides
+    def save(self) -> bool:
+        """
+        Saves the current data to disk if required.  Again, because this is
+        a subclass of Persistent this is taken care of for you.
+        """
+
         import pickle
+
         with open(config.config['weather_data_cachefile'], 'wb') as wf:
             pickle.dump(
                 self.weather_data,
                 wf,
                 pickle.HIGHEST_PROTOCOL,
             )
+        return True