Repository: rado0x54/python-carson-living

Issue:
CarsonAuth.update_token() should return token
Currently implementers that use manual authentication need to make two calls

```
auth = CarsonAuth(data["username"], data["password"])
auth.update_token()
return auth.token
```

Discussion:

Patch:
diff --git a/README.rst b/README.rst
index c3d89da..61886c0 100644
--- a/README.rst
+++ b/README.rst
@@ -32,7 +32,7 @@ Carson Living Python should work against **Python 2.x >= 2.7** and **Python 3.x
 .. code-block::
 
     # Installing from PyPi
-    $ pip install carson-living
+    $ pip install carson_living
 
     # Installing latest development
     $ pip install \
diff --git a/carson_living/auth.py b/carson_living/auth.py
index 637035c..f719251 100644
--- a/carson_living/auth.py
+++ b/carson_living/auth.py
@@ -32,17 +32,35 @@ class CarsonAuth(object):
         _token: current JWT token
         _token_payload: current JWT token payload
         _token_expiration_time: current JWT token expiration time
+        _token_update_cb:
+            gets executed whenever the token gets update to a
+            non-None value.
     """
 
-    def __init__(self, username, password, token=None):
+    def __init__(self, username, password,
+                 initial_token=None, token_update_cb=None):
         self._username = username
         self._password = password
         self._token = None
         self._token_payload = None
         self._token_expiration_time = None
+        self._token_update_cb = None
 
         # Set and init token values
-        self.token = token
+        self.token = initial_token
+
+        # Set token updater after initial token (so it does not fire)
+        self._token_update_cb = token_update_cb
+
+    @property
+    def username(self):
+        """Username
+
+        Returns:
+            the configured username
+
+        """
+        return self._username
 
     @property
     def token(self):
@@ -88,6 +106,9 @@ class CarsonAuth(object):
             self._token_expiration_time = self._token_payload.get('exp')
 
             self._token = token
+
+            if self._token_update_cb is not None:
+                self._token_update_cb(token)
             _LOGGER.info('Set access Token for %s',
                          self._token_payload.get('email', '<no e-mail found>'))
         except InvalidTokenError:
@@ -101,7 +122,7 @@ class CarsonAuth(object):
             CarsonAuthenticationError: On authentication error.
 
         """
-        _LOGGER.info('Getting new access Token for %s', self._username)
+        _LOGGER.info('Getting new access token for %s', self._username)
 
         response = requests.post(
             (API_URI + AUTH_ENDPOINT),
@@ -114,7 +135,9 @@ class CarsonAuth(object):
         try:
             data = default_carson_response_handler(response)
             self.token = data.get('token')
+            return self.token
         except CarsonAPIError as error:
+            _LOGGER.warning('Authentication for %s failed', self._username)
             raise CarsonAuthenticationError(error)
 
     def valid_token(self):
diff --git a/carson_living/carson.py b/carson_living/carson.py
index b0e115f..06cd5dd 100644
--- a/carson_living/carson.py
+++ b/carson_living/carson.py
@@ -24,8 +24,10 @@ class Carson(CarsonAuth):
                 The building properties that are associated with
                 the current user
     """
-    def __init__(self, username, password, token=None):
-        super(Carson, self).__init__(username, password, token)
+    def __init__(self, username, password,
+                 initial_token=None, token_update_cb=None):
+        super(Carson, self).__init__(username, password,
+                                     initial_token, token_update_cb)
 
         self._user = None
         self._buildings = {}
diff --git a/pylintrc b/pylintrc
index bb0966a..31fbf82 100644
--- a/pylintrc
+++ b/pylintrc
@@ -23,3 +23,6 @@ disable=
 reports=no
 
 [EXCEPTIONS]
+
+[SIMILARITIES]
+ignore-imports=yes


Test patch:
diff --git a/tests/test_auth.py b/tests/test_auth.py
index 702edb0..91db067 100644
--- a/tests/test_auth.py
+++ b/tests/test_auth.py
@@ -4,6 +4,12 @@
 import unittest
 import requests_mock
 
+# 2.7 support fallback
+try:
+    from unittest.mock import Mock
+except ImportError:
+    from mock import Mock
+
 from carson_living import (CarsonAuth,
                            CarsonAPIError,
                            CarsonTokenError,
@@ -26,6 +32,8 @@ class TestCarsonAuth(unittest.TestCase):
     def test_auth_init(self):
         """Test default class initialization"""
         auth = CarsonAuth(USERNAME, PASSWORD)
+
+        self.assertEqual(auth.username, USERNAME)
         self.assertIsNone(auth.token)
         self.assertIsNone(auth.token_payload)
         self.assertIsNone(auth.token_expiration_date)
@@ -43,27 +51,40 @@ class TestCarsonAuth(unittest.TestCase):
         """Test default class initialization with token"""
         token, token_payload = get_encoded_token()
 
-        auth = CarsonAuth(USERNAME, PASSWORD, token)
+        mock_token_update_cb = Mock()
+
+        auth = CarsonAuth(USERNAME, PASSWORD, token, mock_token_update_cb)
+
+        self.assertEqual(auth.username, USERNAME)
         self.assertEqual(auth.token, token)
         self.assertEqual(auth.token_payload, token_payload)
         self.assertEqual(auth.token_expiration_date, token_payload.get('exp'))
         self.assertTrue(auth.valid_token())
 
+        # make sure that token update cb is not executed with an initial token.
+        mock_token_update_cb.assert_not_called()
+
     @requests_mock.Mocker()
     def test_update_token_success(self, mock):
         """Test token update"""
         mock.post('https://api.carson.live/api/v1.4.1/auth/login/',
                   text=load_fixture('carson.live', 'carson_login.json'))
 
-        auth = CarsonAuth(USERNAME, PASSWORD)
-        auth.update_token()
+        mock_token_update_cb = Mock()
+
+        auth = CarsonAuth(USERNAME, PASSWORD,
+                          token_update_cb=mock_token_update_cb)
+        token = auth.update_token()
 
+        self.assertEqual(auth.username, USERNAME)
+        self.assertEqual(FIXTURE_TOKEN, token)
         self.assertEqual(FIXTURE_TOKEN, auth.token)
         self.assertTrue(mock.called)
         self.assertEqual(USERNAME,
                          mock.last_request.json().get('username'))
         self.assertEqual(PASSWORD,
                          mock.last_request.json().get('password'))
+        mock_token_update_cb.assert_called_once_with(FIXTURE_TOKEN)
 
     @requests_mock.Mocker()
     def test_update_token_fail(self, mock):
diff --git a/tests/test_carson.py b/tests/test_carson.py
index a7aa944..0baf763 100644
--- a/tests/test_carson.py
+++ b/tests/test_carson.py
@@ -67,6 +67,13 @@ class TestCarson(CarsonUnitTestBase):
             self.c_mock_me['isService'],
             user.is_service)
 
+        self.assertIn(str(user.entity_id), str(user))
+        self.assertIn(user.first_name, str(user))
+        self.assertIn(user.last_name, str(user))
+
+        self.assertIn(user.unique_entity_id, repr(user))
+        self.assertIn("CarsonUser", repr(user))
+
     def test_api_buildings_initialization(self):
         """Test correct buildings entities after initialization"""
         buildings = self.carson.buildings
@@ -143,6 +150,18 @@ class TestCarson(CarsonUnitTestBase):
         self.assertIsNotNone(self.first_building.eagleeye_api)
         self.assertEqual(8, len(self.first_building.eagleeye_api.cameras))
 
+        self.assertIn(str(self.first_building.entity_id),
+                      str(self.first_building))
+        self.assertIn(self.first_building.name,
+                      str(self.first_building))
+        self.assertIn(self.first_building.pmc_name,
+                      str(self.first_building))
+
+        self.assertIn(self.first_building.unique_entity_id,
+                      repr(self.first_building))
+        self.assertIn("CarsonBuilding",
+                      repr(self.first_building))
+
     @requests_mock.Mocker()
     def test_later_carson_update_changes_entities(self, mock):
         """Later calling update changes the entities"""