Make subdirs type clean too.
authorScott <[email protected]>
Wed, 2 Feb 2022 21:09:30 +0000 (13:09 -0800)
committerScott <[email protected]>
Wed, 2 Feb 2022 21:09:30 +0000 (13:09 -0800)
15 files changed:
cached/weather_data.py
cached/weather_forecast.py
collect/bst.py
collect/shared_dict.py
dateparse/dateparse_utils.py
datetime_utils.py
ml/model_trainer.py
smart_home/chromecasts.py
smart_home/device.py
smart_home/lights.py
smart_home/outlets.py
smart_home/registry.py
type/centcount.py
type/money.py
type/rate.py

index 45b6e6efc5d9d3c4cbd07f0996633d835aa4a98a..7b86d029c607074d0fb88949ff9e50641d169401 100644 (file)
@@ -1,12 +1,12 @@
 #!/usr/bin/env python3
 
-from dataclasses import dataclass
 import datetime
 import json
 import logging
 import os
-from typing import Any, List
 import urllib.request
+from dataclasses import dataclass
+from typing import Any, List
 
 from overrides import overrides
 
@@ -27,32 +27,31 @@ cfg.add_argument(
     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
+    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=None):
         if weather_data is not None:
             self.weather_data = weather_data
             return
@@ -72,7 +71,7 @@ class CachedWeatherData(persistent.Persistent):
             "Sand": "🏜️",
             "Ash": "🌋",
             "Squall": "🌬",
-            "Tornado": "🌪️"
+            "Tornado": "🌪️",
         }
         now = datetime.datetime.now()
         dates = set()
@@ -80,7 +79,7 @@ class CachedWeatherData(persistent.Persistent):
         lows = {}
         conditions = {}
         precip = {}
-        param = "id=5786882"   # Bellevue, WA
+        param = "id=5786882"  # Bellevue, WA
         key = "c0b160c49743622f62a9cd3cda0270b3"
         www = urllib.request.urlopen(
             f'http://api.openweathermap.org/data/2.5/weather?zip=98005,us&APPID={key}&units=imperial'
@@ -107,13 +106,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(
@@ -134,9 +133,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
@@ -160,10 +159,7 @@ 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(conditions[dt])
@@ -171,23 +167,24 @@ class CachedWeatherData(persistent.Persistent):
             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=highs[dt],
+                low=lows[dt],
+                precipitation_inches=precip[dt] / 25.4,
+                conditions=conditions[dt],
+                most_common_condition=most_common_condition,
+                icon=icon,
             )
 
     @classmethod
     @overrides
     def load(cls) -> Any:
         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)
@@ -196,6 +193,7 @@ class CachedWeatherData(persistent.Persistent):
     @overrides
     def save(self) -> bool:
         import pickle
+
         with open(config.config['weather_data_cachefile'], 'wb') as wf:
             pickle.dump(
                 self.weather_data,
index b34393832dec04120548aa08fb6822366cfc6ff6..58f53c383cb2425f1114165cfe3c151978e69cf5 100644 (file)
@@ -1,60 +1,59 @@
 #!/usr/bin/env python3
 
-from dataclasses import dataclass
 import datetime
 import logging
 import os
-from typing import Any
 import urllib.request
+from dataclasses import dataclass
+from typing import Any
 
 import astral  # type: ignore
+import pytz
 from astral.sun import sun  # type: ignore
 from bs4 import BeautifulSoup  # type: ignore
 from overrides import overrides
-import pytz
 
 import argparse_utils
 import config
-import datetime_utils
 import dateparse.dateparse_utils as dp
+import datetime_utils
 import persistent
-import text_utils
 import smart_home.thermometers as temps
-
+import text_utils
 
 logger = logging.getLogger(__name__)
 
 cfg = config.add_commandline_args(
     f'Cached Weather Forecast ({__file__})',
-    'Arguments controlling detailed weather rendering'
+    'Arguments controlling detailed weather rendering',
 )
 cfg.add_argument(
     '--weather_forecast_cachefile',
     type=str,
     default=f'{os.environ["HOME"]}/cache/.weather_forecast_cache',
     metavar='FILENAME',
-    help='File in which to cache weather data'
+    help='File in which to cache weather data',
 )
 cfg.add_argument(
     '--weather_forecast_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 WeatherForecast:
-    date: datetime.date                # The date
-    sunrise: datetime.datetime         # Sunrise datetime
-    sunset: datetime.datetime          # Sunset datetime
-    description: str                   # Textual description of weather
+    date: datetime.date  # The date
+    sunrise: datetime.datetime  # Sunrise datetime
+    sunset: datetime.datetime  # Sunset datetime
+    description: str  # Textual description of weather
 
 
[email protected]_autoloaded_singleton()
[email protected]_autoloaded_singleton()  # type: ignore
 class CachedDetailedWeatherForecast(persistent.Persistent):
-    def __init__(self, forecasts = None):
+    def __init__(self, forecasts=None):
         if forecasts is not None:
             self.forecasts = forecasts
             return
@@ -82,8 +81,7 @@ class CachedDetailedWeatherForecast(persistent.Persistent):
         last_dt = now
         dt = now
         for (day, txt) in zip(
-                forecast.find_all('b'),
-                forecast.find_all(class_='col-sm-10 forecast-text')
+            forecast.find_all('b'), forecast.find_all(class_='col-sm-10 forecast-text')
         ):
             last_dt = dt
             try:
@@ -112,20 +110,21 @@ class CachedDetailedWeatherForecast(persistent.Persistent):
                 self.forecasts[dt.date()].description += '\n' + blurb
             else:
                 self.forecasts[dt.date()] = WeatherForecast(
-                    date = dt,
-                    sunrise = sunrise,
-                    sunset = sunset,
-                    description = blurb,
+                    date=dt,
+                    sunrise=sunrise,
+                    sunset=sunset,
+                    description=blurb,
                 )
 
     @classmethod
     @overrides
     def load(cls) -> Any:
         if persistent.was_file_written_within_n_seconds(
-                config.config['weather_forecast_cachefile'],
-                config.config['weather_forecast_stalest_acceptable'].total_seconds(),
+            config.config['weather_forecast_cachefile'],
+            config.config['weather_forecast_stalest_acceptable'].total_seconds(),
         ):
             import pickle
+
             with open(config.config['weather_forecast_cachefile'], 'rb') as rf:
                 weather_data = pickle.load(rf)
                 return cls(weather_data)
@@ -134,6 +133,7 @@ class CachedDetailedWeatherForecast(persistent.Persistent):
     @overrides
     def save(self) -> bool:
         import pickle
+
         with open(config.config['weather_forecast_cachefile'], 'wb') as wf:
             pickle.dump(
                 self.forecasts,
index 72a3b7738b981878b9b08eaba67bca2b33314f4f..9d6525946e8131728896d86f3400c38c5ba528e7 100644 (file)
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 
-from typing import Any, Optional, List
+from typing import Any, Generator, List, Optional
 
 
 class Node(object):
@@ -9,8 +9,8 @@ class Node(object):
         Note: value can be anything as long as it is comparable.
         Check out @functools.total_ordering.
         """
-        self.left = None
-        self.right = None
+        self.left: Optional[Node] = None
+        self.right: Optional[Node] = None
         self.value = value
 
 
@@ -84,16 +84,18 @@ class BinarySearchTree(object):
         """Find helper"""
         if value == node.value:
             return node
-        elif (value < node.value and node.left is not None):
+        elif value < node.value and node.left is not None:
             return self._find(value, node.left)
-        elif (value > node.value and node.right is not None):
+        elif value > node.value and node.right is not None:
             return self._find(value, node.right)
         return None
 
-    def _parent_path(self, current: Node, target: Node):
+    def _parent_path(
+        self, current: Optional[Node], target: Node
+    ) -> List[Optional[Node]]:
         if current is None:
             return [None]
-        ret = [current]
+        ret: List[Optional[Node]] = [current]
         if target.value == current.value:
             return ret
         elif target.value < current.value:
@@ -104,7 +106,7 @@ class BinarySearchTree(object):
             ret.extend(self._parent_path(current.right, target))
             return ret
 
-    def parent_path(self, node: Node) -> Optional[List[Node]]:
+    def parent_path(self, node: Node) -> List[Optional[Node]]:
         """Return a list of nodes representing the path from
         the tree's root to the node argument.  If the node does
         not exist in the tree for some reason, the last element
@@ -456,7 +458,7 @@ class BinarySearchTree(object):
             if node.right is not None:
                 yield from self._iterate_by_depth(node.right, depth - 1)
 
-    def iterate_nodes_by_depth(self, depth: int):
+    def iterate_nodes_by_depth(self, depth: int) -> Generator[Node, None, None]:
         """
         Iterate only the leaf nodes in the tree.
 
@@ -518,13 +520,16 @@ class BinarySearchTree(object):
             return x
 
         path = self.parent_path(node)
+        assert path[-1]
         assert path[-1] == node
         path = path[:-1]
         path.reverse()
         for ancestor in path:
+            assert ancestor
             if node != ancestor.right:
                 return ancestor
             node = ancestor
+        raise Exception()
 
     def _depth(self, node: Node, sofar: int) -> int:
         depth_left = sofar + 1
@@ -569,7 +574,9 @@ class BinarySearchTree(object):
     def height(self):
         return self.depth()
 
-    def repr_traverse(self, padding: str, pointer: str, node: Node, has_right_sibling: bool) -> str:
+    def repr_traverse(
+        self, padding: str, pointer: str, node: Optional[Node], has_right_sibling: bool
+    ) -> str:
         if node is not None:
             viz = f'\n{padding}{pointer}{node.value}'
             if has_right_sibling:
@@ -583,7 +590,9 @@ class BinarySearchTree(object):
             else:
                 pointer_left = "└──"
 
-            viz += self.repr_traverse(padding, pointer_left, node.left, node.right is not None)
+            viz += self.repr_traverse(
+                padding, pointer_left, node.left, node.right is not None
+            )
             viz += self.repr_traverse(padding, pointer_right, node.right, False)
             return viz
         return ""
@@ -619,11 +628,14 @@ class BinarySearchTree(object):
         else:
             pointer_left = "├──"
 
-        ret += self.repr_traverse('', pointer_left, self.root.left, self.root.left is not None)
+        ret += self.repr_traverse(
+            '', pointer_left, self.root.left, self.root.left is not None
+        )
         ret += self.repr_traverse('', pointer_right, self.root.right, False)
         return ret
 
 
 if __name__ == '__main__':
     import doctest
+
     doctest.testmod()
index 0d8e7c2f7a36aa5ddb7c54c72aecddbf56df71c3..7c84c14c073743ea1e452d58393f8f29d280ed23 100644 (file)
@@ -30,14 +30,14 @@ This class is based on https://github.com/luizalabs/shared-memory-dict
 import pickle
 from contextlib import contextmanager
 from functools import wraps
-from multiprocessing import shared_memory, RLock
+from multiprocessing import RLock, shared_memory
 from typing import (
     Any,
     Dict,
     Generator,
-    KeysView,
     ItemsView,
     Iterator,
+    KeysView,
     Optional,
     ValuesView,
 )
index be5e1b5312a7beb83fea7dc7b0137f08059879a3..7ca3cf3123a2a460c375a2ef811575d656c0d572 100755 (executable)
@@ -7,7 +7,6 @@ Parse dates in a variety of formats.
 
 import datetime
 import functools
-import holidays  # type: ignore
 import logging
 import re
 import sys
@@ -16,21 +15,21 @@ from typing import Any, Callable, Dict, Optional
 import antlr4  # type: ignore
 import dateutil.easter
 import dateutil.tz
+import holidays  # type: ignore
 import pytz
 
 import acl
 import bootstrap
+import decorator_utils
+from dateparse.dateparse_utilsLexer import dateparse_utilsLexer  # type: ignore
+from dateparse.dateparse_utilsListener import dateparse_utilsListener  # type: ignore
+from dateparse.dateparse_utilsParser import dateparse_utilsParser  # type: ignore
 from datetime_utils import (
     TimeUnit,
-    n_timeunits_from_base,
-    datetime_to_date,
     date_to_datetime,
+    datetime_to_date,
+    n_timeunits_from_base,
 )
-from dateparse.dateparse_utilsLexer import dateparse_utilsLexer  # type: ignore
-from dateparse.dateparse_utilsListener import dateparse_utilsListener  # type: ignore
-from dateparse.dateparse_utilsParser import dateparse_utilsParser  # type: ignore
-import decorator_utils
-
 
 logger = logging.getLogger(__name__)
 
@@ -225,8 +224,9 @@ class DateParser(dateparse_utilsListener):
         to timezone naive (i.e. tzinfo = None).
         """
         dt = self.datetime
-        if tz is not None:
-            dt = dt.replace(tzinfo=None).astimezone(tz=tz)
+        if dt is not None:
+            if tz is not None:
+                dt = dt.replace(tzinfo=None).astimezone(tz=tz)
         return dt
 
     # -- helpers --
@@ -376,25 +376,25 @@ class DateParser(dateparse_utilsListener):
 
         # Try pytz
         try:
-            tz = pytz.timezone(txt)
-            if tz is not None:
-                return tz
+            tz1 = pytz.timezone(txt)
+            if tz1 is not None:
+                return tz1
         except Exception:
             pass
 
         # Try dateutil
         try:
-            tz = dateutil.tz.gettz(txt)
-            if tz is not None:
-                return tz
+            tz2 = dateutil.tz.gettz(txt)
+            if tz2 is not None:
+                return tz2
         except Exception:
             pass
 
         # Try constructing an offset in seconds
         try:
-            sign = txt[0]
-            if sign == '-' or sign == '+':
-                sign = +1 if sign == '+' else -1
+            txt_sign = txt[0]
+            if txt_sign == '-' or txt_sign == '+':
+                sign = +1 if txt_sign == '+' else -1
                 hour = int(txt[1:3])
                 minute = int(txt[-2:])
                 offset = sign * (hour * 60 * 60) + sign * (minute * 60)
@@ -441,6 +441,7 @@ class DateParser(dateparse_utilsListener):
         # Apply resudual adjustments to times here when we have a
         # datetime.
         self.datetime = self.datetime + self.timedelta
+        assert self.datetime
         self.time = datetime.time(
             self.datetime.hour,
             self.datetime.minute,
@@ -547,7 +548,7 @@ class DateParser(dateparse_utilsListener):
             elif unit == TimeUnit.HOURS:
                 self.timedelta = datetime.timedelta(hours=count)
             else:
-                raise ParseException()
+                raise ParseException(f'Invalid Unit: "{unit}"')
 
     def exitDeltaPlusMinusExpr(
         self, ctx: dateparse_utilsParser.DeltaPlusMinusExprContext
@@ -1027,6 +1028,7 @@ def main() -> None:
             logger.exception(e)
             print("Unrecognized.")
         else:
+            assert dt
             print(dt.strftime('%A %Y/%m/%d %H:%M:%S.%f %Z(%z)'))
     sys.exit(0)
 
index 6f504f6c304b850e830cffab08d0ed8be67fed39..fb859719796c5cc377e328bacf2aecf2b6bb81f6 100644 (file)
@@ -229,7 +229,7 @@ def datetime_to_time(dt: datetime.datetime) -> datetime.time:
     return datetime_to_date_and_time(dt)[1]
 
 
-class TimeUnit(enum.Enum):
+class TimeUnit(enum.IntEnum):
     """An enum to represent units with which we can compute deltas."""
 
     MONDAYS = 0
index 213a1814cff5e98507e30c19e17669ab123886ce..6fc0da071e7dd6e2e026b8bbb7d7780c81015985 100644 (file)
@@ -2,7 +2,6 @@
 
 from __future__ import annotations
 
-from abc import ABC, abstractmethod
 import datetime
 import glob
 import logging
@@ -10,20 +9,21 @@ import os
 import pickle
 import random
 import sys
+import warnings
+from abc import ABC, abstractmethod
 from types import SimpleNamespace
 from typing import Any, List, NamedTuple, Optional, Set, Tuple
-import warnings
 
 import numpy as np
 from sklearn.model_selection import train_test_split  # type:ignore
 from sklearn.preprocessing import MinMaxScaler  # type: ignore
 
-from ansi import bold, reset
 import argparse_utils
 import config
-from decorator_utils import timed
 import executors
 import parallelize as par
+from ansi import bold, reset
+from decorator_utils import timed
 
 logger = logging.getLogger(__file__)
 
@@ -81,8 +81,8 @@ class OutputSpec(NamedTuple):
     model_filename: Optional[str]
     model_info_filename: Optional[str]
     scaler_filename: Optional[str]
-    training_score: float
-    test_score: float
+    training_score: np.float64
+    test_score: np.float64
 
 
 class TrainingBlueprint(ABC):
@@ -131,9 +131,9 @@ class TrainingBlueprint(ABC):
             modelid_to_params[model.get_id()] = str(params)
 
         best_model = None
-        best_score = None
-        best_test_score = None
-        best_training_score = None
+        best_score: Optional[np.float64] = None
+        best_test_score: Optional[np.float64] = None
+        best_training_score: Optional[np.float64] = None
         best_params = None
         for model in smart_future.wait_any(models):
             params = modelid_to_params[model.get_id()]
@@ -170,6 +170,9 @@ class TrainingBlueprint(ABC):
             print(msg)
             logger.info(msg)
 
+        assert best_training_score
+        assert best_test_score
+        assert best_params
         (
             scaler_filename,
             model_filename,
@@ -369,14 +372,14 @@ Testing set score: {test_score:.2f}%"""
                 and input_utils.yn_response("Write the model? [y,n]: ") == "y"
             ):
                 scaler_filename = f"{self.spec.basename}_scaler.sav"
-                with open(scaler_filename, "wb") as f:
-                    pickle.dump(scaler, f)
+                with open(scaler_filename, "wb") as fb:
+                    pickle.dump(scaler, fb)
                 msg = f"Wrote {scaler_filename}"
                 print(msg)
                 logger.info(msg)
                 model_filename = f"{self.spec.basename}_model.sav"
-                with open(model_filename, "wb") as f:
-                    pickle.dump(model, f)
+                with open(model_filename, "wb") as fb:
+                    pickle.dump(model, fb)
                 msg = f"Wrote {model_filename}"
                 print(msg)
                 logger.info(msg)
index a5db86f3f003395d61ee2436688e44eaaa55d963..bd2a80c54c2af85c191cf5d5272df6bcc7d89cf1 100644 (file)
@@ -6,17 +6,18 @@ import atexit
 import datetime
 import logging
 import threading
+from typing import Any, List
 
 import pychromecast
 
-from decorator_utils import memoized
 import smart_home.device as dev
+from decorator_utils import memoized
 
 logger = logging.getLogger(__name__)
 
 
 class BaseChromecast(dev.Device):
-    ccasts = []
+    ccasts: List[Any] = []
     refresh_ts = None
     browser = None
     lock = threading.Lock()
@@ -25,31 +26,32 @@ class BaseChromecast(dev.Device):
         super().__init__(name.strip(), mac.strip(), keywords)
         ip = self.get_ip()
         now = datetime.datetime.now()
-        with BaseChromecast.lock as l:
+        with BaseChromecast.lock:
             if (
-                    BaseChromecast.refresh_ts is None
-                    or (now - BaseChromecast.refresh_ts).total_seconds() > 60
+                BaseChromecast.refresh_ts is None
+                or (now - BaseChromecast.refresh_ts).total_seconds() > 60
             ):
                 logger.debug('Refreshing the shared chromecast info list')
                 if BaseChromecast.browser is not None:
                     BaseChromecast.browser.stop_discovery()
-                BaseChromecast.ccasts, BaseChromecast.browser = pychromecast.get_chromecasts(
-                    timeout=15.0
-                )
+                (
+                    BaseChromecast.ccasts,
+                    BaseChromecast.browser,
+                ) = pychromecast.get_chromecasts(timeout=15.0)
+                assert BaseChromecast.browser
                 atexit.register(BaseChromecast.browser.stop_discovery)
                 BaseChromecast.refresh_ts = now
 
         self.cast = None
         for cc in BaseChromecast.ccasts:
-            if (
-                    cc.cast_info.host == ip
-                    and cc.cast_info.cast_type != 'group'
-            ):
+            if cc.cast_info.host == ip and cc.cast_info.cast_type != 'group':
                 logger.debug(f'Found chromecast at {ip}: {cc}')
                 self.cast = cc
                 self.cast.wait(timeout=1.0)
         if self.cast is None:
-            raise Exception(f'Can\'t find ccast device at {ip}, is that really a ccast device?')
+            raise Exception(
+                f'Can\'t find ccast device at {ip}, is that really a ccast device?'
+            )
 
     def is_idle(self):
         return self.cast.is_idle
@@ -116,4 +118,3 @@ class BaseChromecast(dev.Device):
             f"Chromecast({self.cast.socket_client.host!r}, port={self.cast.socket_client.port!r}, "
             f"device={self.cast.cast_info.friendly_name!r})"
         )
-
index 9675b7c66ed26527d6c389854a8cf488a012f530..02717a343f433414398425c7d16e4939414ecf2a 100644 (file)
@@ -8,17 +8,17 @@ import arper
 
 class Device(object):
     def __init__(
-            self,
-            name: str,
-            mac: str,
-            keywords: Optional[List[str]],
+        self,
+        name: str,
+        mac: str,
+        keywords: Optional[List[str]],
     ):
         self.name = name
         self.mac = mac
         self.keywords = keywords
         self.arper = arper.Arper()
         if keywords is not None:
-            self.kws = keywords.split()
+            self.kws = keywords
         else:
             self.kws = []
 
index 64f2105ffe8a4de0864e95a14d2b05703010a2bb..240e7da84412f089702b7b50bd4ce5b5080ca0eb 100644 (file)
@@ -2,7 +2,6 @@
 
 """Utilities for dealing with the smart lights."""
 
-from abc import abstractmethod
 import datetime
 import json
 import logging
@@ -10,10 +9,11 @@ import os
 import re
 import subprocess
 import sys
+from abc import abstractmethod
 from typing import Any, Dict, List, Optional, Tuple
 
-from overrides import overrides
 import tinytuya as tt
+from overrides import overrides
 
 import ansi
 import argparse_utils
@@ -21,8 +21,8 @@ import arper
 import config
 import logging_utils
 import smart_home.device as dev
-from google_assistant import ask_google, GoogleResponse
-from decorator_utils import timeout, memoized
+from decorator_utils import memoized, timeout
+from google_assistant import GoogleResponse, ask_google
 
 logger = logging.getLogger(__name__)
 
@@ -39,9 +39,7 @@ args.add_argument(
 )
 
 
-@timeout(
-    5.0, use_signals=False, error_message="Timed out waiting for tplink.py"
-)
+@timeout(5.0, use_signals=False, error_message="Timed out waiting for tplink.py")
 def tplink_light_command(command: str) -> bool:
     result = os.system(command)
     signal = result & 0xFF
@@ -69,9 +67,9 @@ class BaseLight(dev.Device):
     def parse_color_string(color: str) -> Optional[Tuple[int, int, int]]:
         m = re.match(
             'r#?([0-9a-fA-F][0-9a-fA-F])([0-9a-fA-F][0-9a-fA-F])([0-9a-fA-F][0-9a-fA-F])',
-            color
+            color,
         )
-        if m is not None and len(m.group) == 3:
+        if m is not None and len(m.groups()) == 3:
             red = int(m.group(0), 16)
             green = int(m.group(1), 16)
             blue = int(m.group(2), 16)
@@ -147,7 +145,9 @@ class GoogleLight(BaseLight):
         r = ask_google(f"is {self.goog_name()} on?")
         if not r.success:
             return False
-        return 'is on' in r.audio_transcription
+        if r.audio_transcription is not None:
+            return 'is on' in r.audio_transcription
+        raise Exception("Can't reach Google?!")
 
     @overrides
     def is_off(self) -> bool:
@@ -163,11 +163,12 @@ class GoogleLight(BaseLight):
 
         # the bookcase one is set to 40% bright
         txt = r.audio_transcription
-        m = re.search(r"(\d+)% bright", txt)
-        if m is not None:
-            return int(m.group(1))
-        if "is off" in txt:
-            return 0
+        if txt is not None:
+            m = re.search(r"(\d+)% bright", txt)
+            if m is not None:
+                return int(m.group(1))
+            if "is off" in txt:
+                return 0
         return None
 
     @overrides
@@ -301,9 +302,7 @@ class TPLinkLight(BaseLight):
     def get_children(self) -> List[str]:
         return self.children
 
-    def command(
-        self, cmd: str, child: str = None, extra_args: str = None
-    ) -> bool:
+    def command(self, cmd: str, child: str = None, extra_args: str = None) -> bool:
         cmd = self.get_cmdline(child) + f"-c {cmd}"
         if extra_args is not None:
             cmd += f" {extra_args}"
@@ -333,9 +332,7 @@ class TPLinkLight(BaseLight):
     def make_color(self, color: str) -> bool:
         raise NotImplementedError
 
-    @timeout(
-        10.0, use_signals=False, error_message="Timed out waiting for tplink.py"
-    )
+    @timeout(10.0, use_signals=False, error_message="Timed out waiting for tplink.py")
     def get_info(self) -> Optional[Dict]:
         cmd = self.get_cmdline() + "-c info"
         out = subprocess.getoutput(cmd)
index c079cfd09d9cbbfbb8595379770ebc1c8d7497ae..d4a4886dd38d1a932494e90f060f4ce884de744d 100644 (file)
@@ -2,7 +2,6 @@
 
 """Utilities for dealing with the smart outlets."""
 
-from abc import abstractmethod
 import asyncio
 import atexit
 import datetime
@@ -12,10 +11,12 @@ import os
 import re
 import subprocess
 import sys
+from abc import abstractmethod
 from typing import Any, Dict, List, Optional
 
 from meross_iot.http_api import MerossHttpClient
 from meross_iot.manager import MerossManager
+from overrides import overrides
 
 import argparse_utils
 import config
@@ -23,8 +24,8 @@ import decorator_utils
 import logging_utils
 import scott_secrets
 import smart_home.device as dev
-from google_assistant import ask_google, GoogleResponse
-from decorator_utils import timeout, memoized
+from decorator_utils import memoized, timeout
+from google_assistant import GoogleResponse, ask_google
 
 logger = logging.getLogger(__name__)
 
@@ -41,9 +42,7 @@ parser.add_argument(
 )
 
 
-@timeout(
-    5.0, use_signals=False, error_message="Timed out waiting for tplink.py"
-)
+@timeout(5.0, use_signals=False, error_message="Timed out waiting for tplink.py")
 def tplink_outlet_command(command: str) -> bool:
     result = os.system(command)
     signal = result & 0xFF
@@ -66,7 +65,6 @@ def tplink_outlet_command(command: str) -> bool:
 class BaseOutlet(dev.Device):
     def __init__(self, name: str, mac: str, keywords: str = "") -> None:
         super().__init__(name.strip(), mac.strip(), keywords)
-        self.info = None
 
     @abstractmethod
     def turn_on(self) -> bool:
@@ -105,27 +103,29 @@ class TPLinkOutlet(BaseOutlet):
         )
         return cmd
 
-    def command(self, cmd: str, extra_args: str = None) -> bool:
+    def command(self, cmd: str, extra_args: str = None, **kwargs) -> bool:
         cmd = self.get_cmdline() + f"-c {cmd}"
         if extra_args is not None:
             cmd += f" {extra_args}"
         return tplink_outlet_command(cmd)
 
+    @overrides
     def turn_on(self) -> bool:
         return self.command('on')
 
+    @overrides
     def turn_off(self) -> bool:
         return self.command('off')
 
+    @overrides
     def is_on(self) -> bool:
         return self.get_on_duration_seconds() > 0
 
+    @overrides
     def is_off(self) -> bool:
         return not self.is_on()
 
-    @timeout(
-        10.0, use_signals=False, error_message="Timed out waiting for tplink.py"
-    )
+    @timeout(10.0, use_signals=False, error_message="Timed out waiting for tplink.py")
     def get_info(self) -> Optional[Dict]:
         cmd = self.get_cmdline() + "-c info"
         out = subprocess.getoutput(cmd)
@@ -161,7 +161,7 @@ class TPLinkOutletWithChildren(TPLinkOutlet):
             for child in self.info["children"]:
                 self.children.append(child["id"])
 
-    # override
+    @overrides
     def get_cmdline(self, child: Optional[str] = None) -> str:
         cmd = (
             f"{config.config['smart_outlets_tplink_location']} -m {self.mac} "
@@ -171,10 +171,9 @@ class TPLinkOutletWithChildren(TPLinkOutlet):
             cmd += f"-x {child} "
         return cmd
 
-    # override
-    def command(
-        self, cmd: str, child: str = None, extra_args: str = None
-    ) -> bool:
+    @overrides
+    def command(self, cmd: str, extra_args: str = None, **kwargs) -> bool:
+        child: Optional[str] = kwargs.get('child', None)
         cmd = self.get_cmdline(child) + f"-c {cmd}"
         if extra_args is not None:
             cmd += f" {extra_args}"
@@ -184,9 +183,11 @@ class TPLinkOutletWithChildren(TPLinkOutlet):
     def get_children(self) -> List[str]:
         return self.children
 
+    @overrides
     def turn_on(self, child: str = None) -> bool:
         return self.command("on", child)
 
+    @overrides
     def turn_off(self, child: str = None) -> bool:
         return self.command("off", child)
 
@@ -218,22 +219,28 @@ class GoogleOutlet(BaseOutlet):
     def parse_google_response(response: GoogleResponse) -> bool:
         return response.success
 
+    @overrides
     def turn_on(self) -> bool:
         return GoogleOutlet.parse_google_response(
-            ask_google('turn {self.goog_name()} on')
+            ask_google(f'turn {self.goog_name()} on')
         )
 
+    @overrides
     def turn_off(self) -> bool:
         return GoogleOutlet.parse_google_response(
-            ask_google('turn {self.goog_name()} off')
+            ask_google(f'turn {self.goog_name()} off')
         )
 
+    @overrides
     def is_on(self) -> bool:
         r = ask_google(f'is {self.goog_name()} on?')
         if not r.success:
             return False
-        return 'is on' in r.audio_transcription
+        if r.audio_transcription is not None:
+            return 'is on' in r.audio_transcription
+        raise Exception('Can\'t talk to Google right now!?')
 
+    @overrides
     def is_off(self) -> bool:
         return not self.is_on()
 
@@ -247,10 +254,13 @@ class MerossWrapper(object):
     this class.
 
     """
+
     def __init__(self):
         self.loop = asyncio.get_event_loop()
         self.email = os.environ.get('MEROSS_EMAIL') or scott_secrets.MEROSS_EMAIL
-        self.password = os.environ.get('MEROSS_PASSWORD') or scott_secrets.MEROSS_PASSWORD
+        self.password = (
+            os.environ.get('MEROSS_PASSWORD') or scott_secrets.MEROSS_PASSWORD
+        )
         self.devices = self.loop.run_until_complete(self.find_meross_devices())
         atexit.register(self.loop.close)
 
@@ -282,8 +292,8 @@ class MerossWrapper(object):
 class MerossOutlet(BaseOutlet):
     def __init__(self, name: str, mac: str, keywords: str = '') -> None:
         super().__init__(name, mac, keywords)
-        self.meross_wrapper = None
-        self.device = None
+        self.meross_wrapper: Optional[MerossWrapper] = None
+        self.device: Optional[Any] = None
 
     def lazy_initialize_device(self):
         """If we make too many calls to Meross they will block us; only talk
@@ -294,23 +304,28 @@ class MerossOutlet(BaseOutlet):
             if self.device is None:
                 raise Exception(f'{self.name} is not a known Meross device?!')
 
+    @overrides
     def turn_on(self) -> bool:
         self.lazy_initialize_device()
-        self.meross_wrapper.loop.run_until_complete(
-            self.device.async_turn_on()
-        )
+        assert self.meross_wrapper
+        assert self.device
+        self.meross_wrapper.loop.run_until_complete(self.device.async_turn_on())
         return True
 
+    @overrides
     def turn_off(self) -> bool:
         self.lazy_initialize_device()
-        self.meross_wrapper.loop.run_until_complete(
-            self.device.async_turn_off()
-        )
+        assert self.meross_wrapper
+        assert self.device
+        self.meross_wrapper.loop.run_until_complete(self.device.async_turn_off())
         return True
 
+    @overrides
     def is_on(self) -> bool:
         self.lazy_initialize_device()
+        assert self.device
         return self.device.is_on()
 
+    @overrides
     def is_off(self) -> bool:
         return not self.is_on()
index 7349081f1b9b1637e2c18db7465b29e82626f54e..16e18ba11bcc5fa19443245f02df546c68d54787 100644 (file)
@@ -8,15 +8,15 @@ import argparse_utils
 import config
 import file_utils
 import logical_search
-import smart_home.device as device
 import smart_home.cameras as cameras
 import smart_home.chromecasts as chromecasts
+import smart_home.device as device
 import smart_home.lights as lights
 import smart_home.outlets as outlets
 
 args = config.add_commandline_args(
     f"Smart Home Registry ({__file__})",
-    "Args related to the smart home configuration registry."
+    "Args related to the smart home configuration registry.",
 )
 args.add_argument(
     '--smart_home_registry_file_location',
@@ -32,9 +32,9 @@ logger = logging.getLogger(__file__)
 
 class SmartHomeRegistry(object):
     def __init__(
-            self,
-            registry_file: Optional[str] = None,
-            filters: List[str] = ['smart'],
+        self,
+        registry_file: Optional[str] = None,
+        filters: List[str] = ['smart'],
     ) -> None:
         self._macs_by_name = {}
         self._keywords_by_name = {}
@@ -44,13 +44,11 @@ class SmartHomeRegistry(object):
 
         # Read the disk config file...
         if registry_file is None:
-            registry_file = config.config[
-                'smart_home_registry_file_location'
-            ]
+            registry_file = config.config['smart_home_registry_file_location']
         assert file_utils.does_file_exist(registry_file)
         logger.debug(f'Reading {registry_file}')
-        with open(registry_file, "r") as f:
-            contents = f.readlines()
+        with open(registry_file, "r") as rf:
+            contents = rf.readlines()
 
         # Parse the contents...
         for line in contents:
index 4e5b8a6aa6b4b8ef61671b93b0284825cdf75cbc..13f14b7f835d2e15679ea3bddd9499398056448e 100644 (file)
@@ -1,27 +1,18 @@
 #!/usr/bin/env python3
 
 import re
-from typing import Optional, TypeVar, Tuple
+from typing import Optional, Tuple, TypeVar
 
 import math_utils
 
 
-T = TypeVar('T', bound='CentCount')
-
-
 class CentCount(object):
     """A class for representing monetary amounts potentially with
     different currencies meant to avoid floating point rounding
     issues by treating amount as a simple integral count of cents.
     """
 
-    def __init__ (
-            self,
-            centcount,
-            currency: str = 'USD',
-            *,
-            strict_mode = False
-    ):
+    def __init__(self, centcount, currency: str = 'USD', *, strict_mode=False):
         self.strict_mode = strict_mode
         if isinstance(centcount, str):
             ret = CentCount._parse(centcount)
@@ -37,7 +28,7 @@ class CentCount(object):
         if not currency:
             self.currency: Optional[str] = None
         else:
-            self.currency: Optional[str] = currency
+            self.currency = currency
 
     def __repr__(self):
         a = float(self.centcount)
@@ -59,8 +50,7 @@ class CentCount(object):
         if isinstance(other, CentCount):
             if self.currency == other.currency:
                 return CentCount(
-                    centcount = self.centcount + other.centcount,
-                    currency = self.currency
+                    centcount=self.centcount + other.centcount, currency=self.currency
                 )
             else:
                 raise TypeError('Incompatible currencies in add expression')
@@ -74,8 +64,7 @@ class CentCount(object):
         if isinstance(other, CentCount):
             if self.currency == other.currency:
                 return CentCount(
-                    centcount = self.centcount - other.centcount,
-                    currency = self.currency
+                    centcount=self.centcount - other.centcount, currency=self.currency
                 )
             else:
                 raise TypeError('Incompatible currencies in add expression')
@@ -90,8 +79,7 @@ class CentCount(object):
             raise TypeError('can not multiply monetary quantities')
         else:
             return CentCount(
-                centcount = int(self.centcount * float(other)),
-                currency = self.currency
+                centcount=int(self.centcount * float(other)), currency=self.currency
             )
 
     def __truediv__(self, other):
@@ -99,8 +87,8 @@ class CentCount(object):
             raise TypeError('can not divide monetary quantities')
         else:
             return CentCount(
-                centcount = int(float(self.centcount) / float(other)),
-                currency = self.currency
+                centcount=int(float(self.centcount) / float(other)),
+                currency=self.currency,
             )
 
     def __int__(self):
@@ -125,8 +113,7 @@ class CentCount(object):
         if isinstance(other, CentCount):
             if self.currency == other.currency:
                 return CentCount(
-                    centcount = other.centcount - self.centcount,
-                    currency = self.currency
+                    centcount=other.centcount - self.centcount, currency=self.currency
                 )
             else:
                 raise TypeError('Incompatible currencies in sub expression')
@@ -135,8 +122,7 @@ class CentCount(object):
                 raise TypeError('In strict_mode only two moneys can be added')
             else:
                 return CentCount(
-                    centcount = int(other) - self.centcount,
-                    currency = self.currency
+                    centcount=int(other) - self.centcount, currency=self.currency
                 )
 
     __rmul__ = __mul__
@@ -148,10 +134,7 @@ class CentCount(object):
         if other is None:
             return False
         if isinstance(other, CentCount):
-            return (
-                self.centcount == other.centcount and
-                self.currency == other.currency
-            )
+            return self.centcount == other.centcount and self.currency == other.currency
         if self.strict_mode:
             raise TypeError("In strict mode only two CentCounts can be compared")
         else:
@@ -196,8 +179,8 @@ class CentCount(object):
     def __hash__(self):
         return self.__repr__
 
-    CENTCOUNT_RE = re.compile("^([+|-]?)(\d+)(\.\d+)$")
-    CURRENCY_RE = re.compile("^[A-Z][A-Z][A-Z]$")
+    CENTCOUNT_RE = re.compile(r"^([+|-]?)(\d+)(\.\d+)$")
+    CURRENCY_RE = re.compile(r"^[A-Z][A-Z][A-Z]$")
 
     @classmethod
     def _parse(cls, s: str) -> Optional[Tuple[int, str]]:
@@ -220,7 +203,7 @@ class CentCount(object):
         return None
 
     @classmethod
-    def parse(cls, s: str) -> T:
+    def parse(cls, s: str) -> 'CentCount':
         chunks = CentCount._parse(s)
         if chunks is not None:
             return CentCount(chunks[0], chunks[1])
index 290c2c86f91a7a6da652b21e670d2579c51ba6c0..d7e6ffa2197c629949ecae30c855df5870cc3a3a 100644 (file)
@@ -1,26 +1,23 @@
 #!/usr/bin/env python3
 
-from decimal import Decimal
 import re
-from typing import Optional, TypeVar, Tuple
+from decimal import Decimal
+from typing import Optional, Tuple, TypeVar
 
 import math_utils
 
 
-T = TypeVar('T', bound='Money')
-
-
 class Money(object):
     """A class for representing monetary amounts potentially with
     different currencies.
     """
 
-    def __init__ (
-            self,
-            amount: Decimal = Decimal("0"),
-            currency: str = 'USD',
-            *,
-            strict_mode = False
+    def __init__(
+        self,
+        amount: Decimal = Decimal("0"),
+        currency: str = 'USD',
+        *,
+        strict_mode=False,
     ):
         self.strict_mode = strict_mode
         if isinstance(amount, str):
@@ -35,7 +32,7 @@ class Money(object):
         if not currency:
             self.currency: Optional[str] = None
         else:
-            self.currency: Optional[str] = currency
+            self.currency = currency
 
     def __repr__(self):
         a = float(self.amount)
@@ -55,10 +52,7 @@ class Money(object):
     def __add__(self, other):
         if isinstance(other, Money):
             if self.currency == other.currency:
-                return Money(
-                    amount = self.amount + other.amount,
-                    currency = self.currency
-                )
+                return Money(amount=self.amount + other.amount, currency=self.currency)
             else:
                 raise TypeError('Incompatible currencies in add expression')
         else:
@@ -66,17 +60,13 @@ class Money(object):
                 raise TypeError('In strict_mode only two moneys can be added')
             else:
                 return Money(
-                    amount = self.amount + Decimal(float(other)),
-                    currency = self.currency
+                    amount=self.amount + Decimal(float(other)), currency=self.currency
                 )
 
     def __sub__(self, other):
         if isinstance(other, Money):
             if self.currency == other.currency:
-                return Money(
-                    amount = self.amount - other.amount,
-                    currency = self.currency
-                )
+                return Money(amount=self.amount - other.amount, currency=self.currency)
             else:
                 raise TypeError('Incompatible currencies in add expression')
         else:
@@ -84,8 +74,7 @@ class Money(object):
                 raise TypeError('In strict_mode only two moneys can be added')
             else:
                 return Money(
-                    amount = self.amount - Decimal(float(other)),
-                    currency = self.currency
+                    amount=self.amount - Decimal(float(other)), currency=self.currency
                 )
 
     def __mul__(self, other):
@@ -93,8 +82,7 @@ class Money(object):
             raise TypeError('can not multiply monetary quantities')
         else:
             return Money(
-                amount = self.amount * Decimal(float(other)),
-                currency = self.currency
+                amount=self.amount * Decimal(float(other)), currency=self.currency
             )
 
     def __truediv__(self, other):
@@ -102,8 +90,7 @@ class Money(object):
             raise TypeError('can not divide monetary quantities')
         else:
             return Money(
-                amount = self.amount / Decimal(float(other)),
-                currency = self.currency
+                amount=self.amount / Decimal(float(other)), currency=self.currency
             )
 
     def __float__(self):
@@ -124,10 +111,7 @@ class Money(object):
     def __rsub__(self, other):
         if isinstance(other, Money):
             if self.currency == other.currency:
-                return Money(
-                    amount = other.amount - self.amount,
-                    currency = self.currency
-                )
+                return Money(amount=other.amount - self.amount, currency=self.currency)
             else:
                 raise TypeError('Incompatible currencies in sub expression')
         else:
@@ -135,8 +119,7 @@ class Money(object):
                 raise TypeError('In strict_mode only two moneys can be added')
             else:
                 return Money(
-                    amount = Decimal(float(other)) - self.amount,
-                    currency = self.currency
+                    amount=Decimal(float(other)) - self.amount, currency=self.currency
                 )
 
     __rmul__ = __mul__
@@ -148,10 +131,7 @@ class Money(object):
         if other is None:
             return False
         if isinstance(other, Money):
-            return (
-                self.amount == other.amount and
-                self.currency == other.currency
-            )
+            return self.amount == other.amount and self.currency == other.currency
         if self.strict_mode:
             raise TypeError("In strict mode only two Moneys can be compared")
         else:
@@ -196,8 +176,8 @@ class Money(object):
     def __hash__(self):
         return self.__repr__
 
-    AMOUNT_RE = re.compile("^([+|-]?)(\d+)(\.\d+)$")
-    CURRENCY_RE = re.compile("^[A-Z][A-Z][A-Z]$")
+    AMOUNT_RE = re.compile(r"^([+|-]?)(\d+)(\.\d+)$")
+    CURRENCY_RE = re.compile(r"^[A-Z][A-Z][A-Z]$")
 
     @classmethod
     def _parse(cls, s: str) -> Optional[Tuple[Decimal, str]]:
@@ -220,7 +200,7 @@ class Money(object):
         return None
 
     @classmethod
-    def parse(cls, s: str) -> T:
+    def parse(cls, s: str) -> 'Money':
         chunks = Money._parse(s)
         if chunks is not None:
             return Money(chunks[0], chunks[1])
index 3161131a027171218f297dcc654e28acd851ca27..64a472650242f863e24a02cd03cb44344db10a76 100644 (file)
@@ -5,11 +5,11 @@ from typing import Optional
 
 class Rate(object):
     def __init__(
-            self,
-            multiplier: Optional[float] = None,
-            *,
-            percentage: Optional[float] = None,
-            percent_change: Optional[float] = None,
+        self,
+        multiplier: Optional[float] = None,
+        *,
+        percentage: Optional[float] = None,
+        percent_change: Optional[float] = None,
     ):
         count = 0
         if multiplier is not None:
@@ -17,7 +17,7 @@ class Rate(object):
                 multiplier = multiplier.replace('%', '')
                 m = float(multiplier)
                 m /= 100
-                self.multiplier = m
+                self.multiplier: float = m
             else:
                 self.multiplier = multiplier
             count += 1
@@ -78,10 +78,7 @@ class Rate(object):
     def __hash__(self):
         return self.multiplier
 
-    def __repr__(self,
-                 *,
-                 relative=False,
-                 places=3):
+    def __repr__(self, *, relative=False, places=3):
         if relative:
             percentage = (self.multiplier - 1.0) * 100.0
         else: