Repository: TheFriendlyCoder/friendlypins

Issue:
lazy load user data from api
Currently, all API calls in our library are funnelled through the "current user" object (ie: api.user()) and this property pre-caches the user profile data for the current user prior to generating the user object returned to the caller. For cases where the user profile data is unnecessary, this forces the caller to perform an initial get request that may be completely superfluous.

In an effort to make the API as efficient as possible in it's use of API calls (ie: to optimize the calls to stay within any rate limits the calling code may have) we should make this pre-caching of the user profile optional, lazy loading the data only when needed.

Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index adb9ca9..917bbc2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,7 +10,7 @@ build/
 # Temporary unit test files
 htmlcov/
 .tox/
-.coverage
+.coverage*
 docs/_*
 docs/api/*
 htmldocs/*
diff --git a/src/friendlypins/api.py b/src/friendlypins/api.py
index 3d382cc..81bc8b0 100644
--- a/src/friendlypins/api.py
+++ b/src/friendlypins/api.py
@@ -1,5 +1,4 @@
 """Primary entry point for the Friendly Pinterest library"""
-from __future__ import print_function
 import logging
 from friendlypins.user import User
 from friendlypins.utils.rest_io import RestIO
@@ -21,24 +20,7 @@ class API(object):  # pylint: disable=too-few-public-methods
     @property
     def user(self):
         """User: Gets all primitives associated with the authenticated user"""
-        self._log.debug("Getting authenticated user details...")
-
-        fields = ",".join([
-            "id",
-            "username",
-            "first_name",
-            "last_name",
-            "bio",
-            "created_at",
-            "counts",
-            "image",
-            "account_type",
-            "url"
-        ])
-        result = self._io.get("me", {"fields": fields})
-        assert 'data' in result
-
-        return User(result['data'], self._io)
+        return User("me", self._io)
 
     @property
     def rate_limit_refresh(self):
diff --git a/src/friendlypins/user.py b/src/friendlypins/user.py
index 5aea1c9..0d8670d 100644
--- a/src/friendlypins/user.py
+++ b/src/friendlypins/user.py
@@ -7,15 +7,53 @@ from friendlypins.board import Board
 class User(object):
     """Abstraction around a Pinterest user and their associated data"""
 
-    def __init__(self, data, rest_io):
+    def __init__(self, url, rest_io):
         """
         Args:
-            data (dict): JSON data parsed from the API
+            url (str): URL for this user, relative to the API root
             rest_io (RestIO): reference to the Pinterest REST API
         """
         self._log = logging.getLogger(__name__)
-        self._data = data
         self._io = rest_io
+        self._relative_url = url
+        self._data_cache = None
+
+    def refresh(self):
+        """Updates cached response data describing the state of this user
+
+        NOTE: This method simply clears the internal cache, and updated
+        information will automatically be pulled on demand as additional
+        queries are made through the API"""
+        self._data_cache = None
+
+    @property
+    def _data(self):
+        """dict: JSON response containing details of the users' profile
+
+        This internal helper caches the user profile data to minimize the
+        number of calls to the REST API, to make more efficient use of rate
+        limitations.
+        """
+        if self._data_cache is not None:
+            return self._data_cache["data"]
+        self._log.debug("Getting authenticated user details...")
+
+        fields = ",".join([
+            "id",
+            "username",
+            "first_name",
+            "last_name",
+            "bio",
+            "created_at",
+            "counts",
+            "image",
+            "account_type",
+            "url"
+        ])
+        self._data_cache = self._io.get(self._relative_url, {"fields": fields})
+        assert 'data' in self._data_cache
+        return self._data_cache["data"]
+
 
     def __str__(self):
         return json.dumps(dict(self._data), sort_keys=True, indent=4)
@@ -67,7 +105,7 @@ class User(object):
     @property
     def boards(self):
         """Board: Generator for iterating over the boards owned by this user"""
-        self._log.debug('Loading boards for user %s...', self.name)
+        self._log.debug('Loading boards for user %s...', self._relative_url)
 
         properties = {
             "fields": ','.join([
@@ -84,7 +122,8 @@ class User(object):
             ])
         }
 
-        for cur_page in self._io.get_pages("me/boards", properties):
+        board_url = "{0}/boards".format(self._relative_url)
+        for cur_page in self._io.get_pages(board_url, properties):
             assert 'data' in cur_page
 
             for cur_item in cur_page['data']:


Test patch:
diff --git a/tests/test_console_actions.py b/tests/test_console_actions.py
index 3097767..7c0a6af 100644
--- a/tests/test_console_actions.py
+++ b/tests/test_console_actions.py
@@ -456,4 +456,9 @@ def test_create_board(rest_io):
     res = create_board("1234abcd", expected_name)
 
     assert res == 0
-    mock_response.get.assert_called_once()
+    # Now that we lazy load user data we should never need to call get
+    # to create a board
+    assert mock_response.get.call_count == 0
+
+    # The post operation should have occurred once to create the board
+    mock_response.post.assert_called_once()
diff --git a/tests/test_user.py b/tests/test_user.py
index 9659aac..ffacb62 100644
--- a/tests/test_user.py
+++ b/tests/test_user.py
@@ -21,13 +21,43 @@ def test_user_properties():
     }
 
     mock_io = mock.MagicMock()
-    obj = User(data, mock_io)
+    mock_io.get.return_value = {"data": data}
+    obj = User("me", mock_io)
     assert expected_url == obj.url
     assert expected_firstname == obj.first_name
     assert expected_lastname == obj.last_name
     assert expected_id == obj.unique_id
     assert expected_board_count == obj.num_boards
     assert expected_pin_count == obj.num_pins
+    mock_io.get.assert_called_once()
+
+
+def test_cache_refresh():
+    expected_url = 'https://www.pinterest.com/MyUserName/'
+    data = {
+        'url': expected_url,
+    }
+
+    mock_io = mock.MagicMock()
+    mock_io.get.return_value = {"data": data}
+    obj = User("me", mock_io)
+    # If we make multiple requests for API data, we should only get
+    # a single hit to the remote API endpoint
+    assert expected_url == obj.url
+    assert expected_url == obj.url
+    mock_io.get.assert_called_once()
+
+    # Calling refresh should clear our internal response cache
+    # which should not require any additional API calls
+    obj.refresh()
+    mock_io.get.assert_called_once()
+
+    # Subsequent requests for additional data should reload the cache,
+    # and then preserve / reuse the cache data for all subsequent calls,
+    # limiting the number of remote requests
+    assert expected_url == obj.url
+    assert expected_url == obj.url
+    assert mock_io.get.call_count == 2
 
 
 def test_get_boards():