diff --git a/CHANGES.rst b/CHANGES.rst index b9d49bf..31c0e7b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,15 @@ +v3.0.0 +====== + +Project no longer exposes a "cache" (or related options for cache-path). +Instead, state from cookies from the API is stored in an "app data" +path (platform-specific). + +It's no longer possible to disable the "cache". Cookies are persisted +unconditionally. + +As a result, a UUID is persisted only if an API login succeeded. + v2.0.0 ====== 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 d92e53a..c3f3c9f 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.json').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,22 +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 = { - 'uuid': str(uuid.uuid1()), - } - - # 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() @@ -101,7 +92,7 @@ def login(self, username=None, password=None, mfa_code=None): # noqa: C901 login_data = { 'id': username, 'password': password, - 'uuid': self._cache['uuid'], + 'uuid': self._session.cookies.get('uuid') or str(uuid.uuid1()), } if mfa_code is not None: @@ -119,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() @@ -332,29 +318,10 @@ def events(self): @property def uuid(self): """Get the UUID.""" - return self._cache['uuid'] + return self._session.cookies['uuid'] def _get_session(self): # Perform a generic update so we know we're logged in 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 8666718..abab774 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,9 +27,11 @@ install_requires = jaraco.collections jaraco.context jaraco.classes + jaraco.net >= 9 more_itertools importlib_resources bx_py_utils + app_paths [options.packages.find] exclude = @@ -58,7 +60,7 @@ testing = pytest-enabler >= 1.3 # local - requests_mock>=1.3.0 + requests_mock types-requests jaraco.collections >= 3.6 diff --git a/tests/test_abode.py b/tests/test_abode.py index 1080fb1..f102cb5 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,10 +487,11 @@ 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.""" - m.post(urls.LOGIN, json=LOGIN.post_response_ok()) + cookies = dict(SESSION='COOKIE') + m.post(urls.LOGIN, json=LOGIN.post_response_ok(), cookies=cookies) m.get(urls.OAUTH_TOKEN, json=OAUTH_CLAIMS.get_response_ok()) m.post(urls.LOGOUT, json=LOGOUT.post_response_ok()) m.get(urls.DEVICES, json=DEVICES.EMPTY_DEVICE_RESPONSE) @@ -497,27 +503,20 @@ def tests_cookies(self, m): password='buzz', auto_login=False, get_devices=False, - disable_cache=False, - cache_path=self.cache_path, ) - # Mock cookie created by Abode after login - cookie = requests.cookies.create_cookie(name='SESSION', value='COOKIE') - - client._session.cookies.set_cookie(cookie) - client.login() # Test that our cookies are fully realized prior to login - assert client._cache['uuid'] is not None - 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.json' + assert cookies_file.exists() - # Copy our current cookies file and data - first_cookies_data = client._cache + # Copy the current cookies + saved_cookies = client._session.cookies # New client reads in old data client = jaraco.abode.Client( @@ -525,50 +524,47 @@ 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._cache['uuid'] == first_cookies_data['uuid'] + 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.""" - m.post(urls.LOGIN, json=LOGIN.post_response_ok()) + cookies = dict(SESSION='COOKIE') + m.post(urls.LOGIN, json=LOGIN.post_response_ok(), cookies=cookies) m.get(urls.OAUTH_TOKEN, json=OAUTH_CLAIMS.get_response_ok()) m.post(urls.LOGOUT, json=LOGOUT.post_response_ok()) m.get(urls.DEVICES, json=DEVICES.EMPTY_DEVICE_RESPONSE) 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.json' # 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['uuid'] is not None + # 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.""" - m.post(urls.LOGIN, json=LOGIN.post_response_ok()) + cookies = dict(SESSION='COOKIE') + m.post(urls.LOGIN, json=LOGIN.post_response_ok(), cookies=cookies) m.get(urls.OAUTH_TOKEN, json=OAUTH_CLAIMS.get_response_ok()) m.post(urls.LOGOUT, json=LOGOUT.post_response_ok()) m.get(urls.DEVICES, json=DEVICES.EMPTY_DEVICE_RESPONSE) 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.json').write_text('invalid cookies') # Cookies are created empty_client = jaraco.abode.Client( @@ -576,10 +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['uuid'] is not None + assert empty_client._session.cookies diff --git a/tox.ini b/tox.ini index ff8ba49..c568abd 100644 --- a/tox.ini +++ b/tox.ini @@ -17,6 +17,9 @@ commands = usedevelop = True extras = testing +deps = + # workaround for jamielennox/requests-mock#17 + requests_mock@git+https://github.com/jaraco/requests-mock@bugfix-17 [testenv:docs] extras =