From 12e22dee8e02973c1d95c4a17f0aa60390d8e398 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Mon, 13 Apr 2020 18:53:37 -0400 Subject: [PATCH] Load credentials from json file --- README.rst | 17 +++++++++++++++++ blinkpy/blinkpy.py | 28 ++++++++++++++++++++++++++-- tests/test_blinkpy.py | 41 +++++++++++++++++++++++++++++++++++++++++ tox.ini | 2 +- 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 14a93eb..7b0eb00 100644 --- a/README.rst +++ b/README.rst @@ -55,6 +55,23 @@ The simplest way to use this package from a terminal is to call ``Blink.start()` If you would like to log in without setting up the cameras or system, you can simply call the ``Blink.login()`` function which will prompt for a username and password and then authenticate with the server. This is useful if you want to avoid use of the ``start()`` function which simply acts as a wrapper for more targeted API methods. +In addition, you can also save your credentials in a json file and initialize Blink with the credential file as follows: + +.. code:: python + + from blinkpy import blinkpy + blink = blinkpy.Blink(cred_file="path/to/credentials.json") + blink.start() + +The credential file must be json formatted with a ``username`` and ``password`` key like follows: + +.. code:: json + + { + 'username': 'YOUR USER NAME', + 'password': 'YOUR PASSWORD' + } + Cameras are instantiated as individual ``BlinkCamera`` classes within a ``BlinkSyncModule`` instance. All of your sync modules are stored within the ``Blink.sync`` dictionary and can be accessed using the name of the sync module as the key (this is the name of your sync module in the Blink App). The below code will display cameras and their available attributes: diff --git a/blinkpy/blinkpy.py b/blinkpy/blinkpy.py index 05fb0a2..8106d16 100644 --- a/blinkpy/blinkpy.py +++ b/blinkpy/blinkpy.py @@ -17,6 +17,7 @@ import os.path import time import getpass import logging +import json from shutil import copyfileobj from requests.structures import CaseInsensitiveDict @@ -42,6 +43,7 @@ class Blink(): """Class to initialize communication.""" def __init__(self, username=None, password=None, + cred_file=None, refresh_rate=DEFAULT_REFRESH, motion_interval=DEFAULT_MOTION_INTERVAL, legacy_subdomain=False): @@ -50,6 +52,10 @@ class Blink(): :param username: Blink username (usually email address) :param password: Blink password + :param cred_file: JSON formatted file to store credentials. + If username and password are given, file + is ignored. Otherwise, username and password + are loaded from file. :param refresh_rate: Refresh rate of blink information. Defaults to 15 (seconds) :param motion_interval: How far back to register motion in minutes. @@ -62,6 +68,7 @@ class Blink(): """ self._username = username self._password = password + self._cred_file = cred_file self._token = None self._auth_header = None self._host = None @@ -117,8 +124,25 @@ class Blink(): def login(self): """Prompt user for username and password.""" - self._username = input("Username:") - self._password = getpass.getpass("Password:") + if self._cred_file is not None and os.path.isfile(self._cred_file): + try: + with open(self._cred_file, 'r') as json_file: + creds = json.load(json_file) + self._username = creds['username'] + self._password = creds['password'] + except ValueError: + _LOGGER.error("Improperly formated json file %s.", + self._cred_file, + exc_info=True) + return False + except KeyError: + _LOGGER.error("JSON file information incomplete %s.", + exc_info=True) + return False + else: + self._username = input("Username:") + self._password = getpass.getpass("Password:") + if self.get_auth_token(): _LOGGER.debug("Login successful!") return True diff --git a/tests/test_blinkpy.py b/tests/test_blinkpy.py index 627b359..613ac23 100644 --- a/tests/test_blinkpy.py +++ b/tests/test_blinkpy.py @@ -79,6 +79,47 @@ class TestBlinkSetup(unittest.TestCase): # pylint: disable=protected-access self.assertEqual(self.blink_no_cred._password, PASSWORD) + @mock.patch('blinkpy.blinkpy.getpass.getpass') + @mock.patch('blinkpy.blinkpy.Blink.get_auth_token') + def test_no_cred_file(self, getpwd, getauth, mock_sess): + """Check that normal login occurs when cred file doesn't exist.""" + # pylint: disable=protected-access + self.blink._cred_file = '/tmp/fake.file' + getpwd.return_value = PASSWORD + getauth.return_value = True + with mock.patch('builtins.input', return_value=USERNAME): + self.assertTrue(self.blink.login()) + + def test_exit_on_missing_json(self, mock_sess): + """Test that we fail on missing json data.""" + # pylint: disable=protected-access + self.blink._cred_file = '/tmp/fake.file' + with mock.patch('os.path.isfile', return_value=True): + with mock.patch('builtins.open', mock.mock_open(read_data="{}")): + self.assertFalse(self.blink.login()) + + def test_exit_on_bad_json(self, mock_sess): + """Test that we fail on bad json format.""" + # pylint: disable=protected-access + self.blink._cred_file = '/tmp/fake.file' + with mock.patch('os.path.isfile', return_value=True): + with mock.patch('builtins.open', mock.mock_open(read_data='{]')): + self.assertFalse(self.blink.login()) + + @mock.patch('blinkpy.blinkpy.json.load') + def test_cred_file(self, mockjson, mock_sess): + """Test that loading credential file works.""" + # pylint: disable=protected-access + self.blink_no_cred._cred_file = '/tmp/fake.file' + mockjson.return_value = {'username': 'foo', 'password': 'bar'} + with mock.patch('os.path.isfile', return_value=True): + with mock.patch('builtins.open', mock.mock_open(read_data='')): + self.assertTrue(self.blink_no_cred.login()) + # pylint: disable=protected-access + self.assertEqual(self.blink_no_cred._username, 'foo') + # pylint: disable=protected-access + self.assertEqual(self.blink_no_cred._password, 'bar') + def test_bad_request(self, mock_sess): """Check that we raise an Exception with a bad request.""" self.blink.session = create_session() diff --git a/tox.ini b/tox.ini index f1b8dfc..55f72b1 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ setenv = LANG=en_US.UTF-8 PYTHONPATH = {toxinidir} commands = - pytest --timeout=9 --durations=10 --cov=blinkpy --cov-report=xml {posargs} + pytest --timeout=9 --durations=10 --cov=blinkpy --cov-report=xml --cov-report=term-missing {posargs} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/requirements_test.txt