#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# © Copyright 2021-2022, Scott Gasch
+
+"""A cache of weather data for Bellevue, WA.
+:class:`CachedWeatherData` class that derives from :class:`Persistent`
+so that, on creation, the decorator transparently pulls in data from
+disk, if possible, to avoid a network request.
+"""
-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__)
type=str,
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 of the forecast"""
+
+ high: float
+ """The predicted high temperature in F"""
+
+ low: float
+ """The predicted low temperature 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 of the day"""
+ icon: str
+ """An icon representing the most common condition of the day"""
+
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
"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'
)
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(
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
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(conditions[dt])
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