From a7de8ed872d4c2fd208f74c0b3686997bfd6ccd3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 30 Nov 2022 21:42:57 -0500 Subject: [PATCH] Replace manual cache with shelf-backed cookie jar. --- conftest.py | 3 +- jaraco/abode/cache.py | 22 ------------ jaraco/abode/cli.py | 9 ----- jaraco/abode/client.py | 57 +++++++------------------------ jaraco/abode/collections.py | 11 ------ jaraco/abode/config.py | 4 +++ jaraco/abode/helpers/constants.py | 2 -- setup.cfg | 1 + tests/test_abode.py | 52 ++++++++++++++-------------- 9 files changed, 45 insertions(+), 116 deletions(-) delete mode 100644 jaraco/abode/cache.py delete mode 100644 jaraco/abode/collections.py create mode 100644 jaraco/abode/config.py diff --git a/conftest.py b/conftest.py index 1af9b57..e6c3048 100644 --- a/conftest.py +++ b/conftest.py @@ -10,7 +10,8 @@ def instance_client(request): return request.instance.client = jaraco.abode.Client( - username='foobar', password='deadbeef', disable_cache=True + username='foobar', + password='deadbeef', ) diff --git a/jaraco/abode/cache.py b/jaraco/abode/cache.py deleted file mode 100644 index 5fde99c..0000000 --- a/jaraco/abode/cache.py +++ /dev/null @@ -1,22 +0,0 @@ -import pickle -import logging - - -log = logging.getLogger(__name__) - - -def save_cache(data, filename): - """Save cookies to a file.""" - with open(filename, 'wb') as handle: - pickle.dump(data, handle) - - -def load_cache(filename): - """Load cookies from a file.""" - with open(filename, 'rb') as handle: - try: - return pickle.load(handle) - except EOFError: - log.warning("Empty pickle file: %s", filename) - except (pickle.UnpicklingError, ValueError): - log.warning("Corrupted pickle file: %s", filename) diff --git a/jaraco/abode/cli.py b/jaraco/abode/cli.py index 4254280..69a95a8 100644 --- a/jaraco/abode/cli.py +++ b/jaraco/abode/cli.py @@ -15,7 +15,6 @@ import jaraco.abode from . import Client from .helpers import urls -from .helpers import constants as CONST from .helpers import timeline as TIMELINE _LOGGER = logging.getLogger('abodecl') @@ -75,13 +74,6 @@ def build_parser(): parser.add_argument('--mfa', help='Multifactor authentication code') - parser.add_argument( - '--cache', - metavar='pickle_file', - help='Create/update/use a pickle cache for the username and password.', - default=CONST.CACHE_PATH, - ) - parser.add_argument( '--mode', help='Output current alarm mode', @@ -222,7 +214,6 @@ def _create_client_instance(args): username=args.username, password=args.password, get_devices=args.mfa is None, - cache_path=args.cache, ) diff --git a/jaraco/abode/client.py b/jaraco/abode/client.py index d6a9d67..f7035a9 100644 --- a/jaraco/abode/client.py +++ b/jaraco/abode/client.py @@ -3,12 +3,13 @@ """ import logging -import os import uuid from more_itertools import always_iterable from requests_toolbelt import sessions from requests.exceptions import RequestException +from jaraco.net.http import cookies +from jaraco.functools import retry import jaraco from .automation import Automation @@ -18,15 +19,23 @@ from .helpers import urls from .helpers import constants as CONST from .helpers import errors as ERROR -from . import collections as COLLECTIONS -from . import cache as CACHE from .devices.base import Device from . import settings +from . import config _LOGGER = logging.getLogger(__name__) +@retry( + retries=1, + cleanup=lambda: config.paths.user_data.joinpath('cookies.db').unlink(), + trap=Exception, +) +def _cookies(): + return cookies.ShelvedCookieJar.create(config.paths.user_data) + + class Client: """Client to an Abode system.""" @@ -37,16 +46,12 @@ def __init__( auto_login=False, get_devices=False, get_automations=False, - cache_path=CONST.CACHE_PATH, - disable_cache=False, ): """Init Abode object.""" self._session = None self._token = None self._panel = None self._user = None - self._cache_path = cache_path - self._disable_cache = disable_cache self._username = username self._password = password @@ -58,20 +63,8 @@ def __init__( self._automations = None - # Create a requests session to persist the cookies self._session = sessions.BaseUrlSession(urls.BASE) - - # Create a new cache template - self._cache = {} - - # Load and merge an existing cache - if not disable_cache: - self._load_cache() - - # Load persisted cookies (which contains the UUID and the session ID) - # if available - if self._cache.get('cookies'): - self._session.cookies = self._cache['cookies'] + self._session.cookies = _cookies() if auto_login: self.login() @@ -117,11 +110,6 @@ def login(self, username=None, password=None, mfa_code=None): # noqa: C901 raise AuthenticationException(ERROR.UNKNOWN_MFA_TYPE) - # Persist cookies (which contains the UUID and the session ID) to disk - if self._session.cookies.get_dict(): - self._cache['cookies'] = self._session.cookies - self._save_cache() - oauth_response = self._session.get(urls.OAUTH_TOKEN) AuthenticationException.raise_for(oauth_response) oauth_response_object = oauth_response.json() @@ -337,22 +325,3 @@ def _get_session(self): self.send_request("get", urls.PANEL) return self._session - - def _load_cache(self): - """Load existing cache and merge for updating if required.""" - if not self._disable_cache and os.path.exists(self._cache_path): - _LOGGER.debug("Cache found at: %s", self._cache_path) - loaded_cache = CACHE.load_cache(self._cache_path) - - if loaded_cache: - COLLECTIONS.update(self._cache, loaded_cache) - else: - _LOGGER.debug("Removing invalid cache file: %s", self._cache_path) - os.remove(self._cache_path) - - self._save_cache() - - def _save_cache(self): - """Trigger a cache save.""" - if not self._disable_cache: - CACHE.save_cache(self._cache, self._cache_path) diff --git a/jaraco/abode/collections.py b/jaraco/abode/collections.py deleted file mode 100644 index 56c8f28..0000000 --- a/jaraco/abode/collections.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Collection routines.""" - -from collections.abc import Mapping, MutableMapping - - -def update(target: MutableMapping, merge: Mapping) -> MutableMapping: - """Recursively merge items from merge into target.""" - for key, value in merge.items(): - recurse = key in target and isinstance(target[key], Mapping) - target[key] = update(target[key], value) if recurse else value - return target diff --git a/jaraco/abode/config.py b/jaraco/abode/config.py new file mode 100644 index 0000000..5682978 --- /dev/null +++ b/jaraco/abode/config.py @@ -0,0 +1,4 @@ +import app_paths + + +paths = app_paths.AppPaths.get_paths(appname='Abode', appauthor=False) diff --git a/jaraco/abode/helpers/constants.py b/jaraco/abode/helpers/constants.py index 9b2fb9f..01ab143 100644 --- a/jaraco/abode/helpers/constants.py +++ b/jaraco/abode/helpers/constants.py @@ -1,5 +1,3 @@ -CACHE_PATH = './abode.pickle' - # NOTIFICATION CONSTANTS SOCKETIO_URL = 'wss://my.goabode.com/socket.io/' diff --git a/setup.cfg b/setup.cfg index ff1ecac..10ee23b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ install_requires = more_itertools importlib_resources bx_py_utils + app_paths [options.packages.find] exclude = diff --git a/tests/test_abode.py b/tests/test_abode.py index c7c26a5..16010f6 100644 --- a/tests/test_abode.py +++ b/tests/test_abode.py @@ -3,8 +3,6 @@ Tests the system initialization and attributes of the main Abode system. """ -import os - import pytest import requests @@ -12,6 +10,7 @@ import jaraco.abode.helpers.constants as CONST from jaraco.abode.helpers import urls from jaraco.abode import settings +from jaraco.abode import config from . import mock as MOCK from .mock import login as LOGIN @@ -24,14 +23,22 @@ @pytest.fixture -def cache_path(tmp_path, request): - request.instance.cache_path = tmp_path / 'cache.pickle' +def data_path(tmp_path, monkeypatch): + class Paths: + user_data_path = tmp_path / 'user_data' + + @property + def user_data(self): + self.user_data_path.mkdir(exist_ok=True) + return self.user_data_path + + monkeypatch.setattr(config, 'paths', Paths()) @pytest.fixture(autouse=True) def abode_objects(request): self = request.instance - self.client_no_cred = jaraco.abode.Client(disable_cache=True) + self.client_no_cred = jaraco.abode.Client() USERNAME = 'foobar' @@ -88,7 +95,6 @@ def tests_auto_login(self, m): password='buzz', auto_login=True, get_devices=False, - disable_cache=True, ) assert client._username == 'fizz' @@ -123,7 +129,6 @@ def tests_auto_fetch(self, m): auto_login=False, get_devices=True, get_automations=True, - disable_cache=True, ) assert client._username == 'fizz' @@ -482,7 +487,7 @@ def tests_siren_settings(self, m): with pytest.raises(jaraco.abode.Exception): self.client.set_setting(settings.SIREN_TAMPER_SOUNDS, "foobar") - @pytest.mark.usefixtures('cache_path') + @pytest.mark.usefixtures('data_path') def tests_cookies(self, m): """Check that cookies are saved and loaded successfully.""" cookies = dict(SESSION='COOKIE') @@ -498,18 +503,17 @@ def tests_cookies(self, m): password='buzz', auto_login=False, get_devices=False, - disable_cache=False, - cache_path=self.cache_path, ) client.login() # Test that our cookies are fully realized prior to login - assert client._cache['cookies'] is not None + assert client._session.cookies # Test that we now have a cookies file - assert os.path.exists(self.cache_path) + cookies_file = config.paths.user_data / 'cookies.db' + assert cookies_file.exists() # Copy the current cookies saved_cookies = client._session.cookies @@ -520,14 +524,12 @@ def tests_cookies(self, m): password='buzz', auto_login=False, get_devices=False, - disable_cache=False, - cache_path=self.cache_path, ) # Test that the cookie data is the same - assert client._session.cookies == saved_cookies + assert str(client._session.cookies) == str(saved_cookies) - @pytest.mark.usefixtures('cache_path') + @pytest.mark.usefixtures('data_path') def test_empty_cookies(self, m): """Check that empty cookies file is loaded successfully.""" cookies = dict(SESSION='COOKIE') @@ -538,22 +540,20 @@ def test_empty_cookies(self, m): m.get(urls.PANEL, json=PANEL.get_response_ok()) # Create an empty file - self.cache_path.write_text('') + cookie_file = config.paths.user_data / 'cookies.db' # Cookies are created - empty_client = jaraco.abode.Client( + jaraco.abode.Client( username='fizz', password='buzz', auto_login=True, get_devices=False, - disable_cache=False, - cache_path=self.cache_path, ) - # Test that some cache exists - assert empty_client._cache['cookies'] + # Test that some cookie data exists + assert cookie_file.read_bytes() - @pytest.mark.usefixtures('cache_path') + @pytest.mark.usefixtures('data_path') def test_invalid_cookies(self, m): """Check that empty cookies file is loaded successfully.""" cookies = dict(SESSION='COOKIE') @@ -564,7 +564,7 @@ def test_invalid_cookies(self, m): m.get(urls.PANEL, json=PANEL.get_response_ok()) # Create an invalid pickle file - self.cache_path.write_text('Invalid file goes here') + config.paths.user_data.joinpath('cookies.db').write_text('invalid cookies') # Cookies are created empty_client = jaraco.abode.Client( @@ -572,9 +572,7 @@ def test_invalid_cookies(self, m): password='buzz', auto_login=True, get_devices=False, - disable_cache=False, - cache_path=self.cache_path, ) # Test that some cache exists - assert empty_client._cache['cookies'] + assert empty_client._session.cookies