Repository: TheFriendlyCoder/friendlypins

Issue:
Update API to auto-load header data
The REST API abstraction layer attempts to auto-load the header data produced by the pinterest API, caching it for future reference, however in certain situations the caching mechanism doesn't 't work. Specifically, only certain method calls on the restio class are configured to populate the internal header cache. To help compensate for more advanced use cases we should update the **headers** property to detect when the internal cache has not yet been populated, and auto-populate the cache.

Discussion:

Patch:
diff --git a/src/friendlypins/api.py b/src/friendlypins/api.py
index 4b014c9..03a92f2 100644
--- a/src/friendlypins/api.py
+++ b/src/friendlypins/api.py
@@ -48,11 +48,7 @@ class API(object):  # pylint: disable=too-few-public-methods
 
         :rtype: :class:`datetime.datetime`
         """
-        try:
-            if self._io.headers is None:
-                self._io.get("me")
-        finally:
-            return self._io.headers.time_to_refresh  # pylint: disable=lost-exception
+        return self._io.headers.time_to_refresh
 
     @property
     def transaction_limit(self):
@@ -60,11 +56,7 @@ class API(object):  # pylint: disable=too-few-public-methods
 
         :rtype: :class:`int`
         """
-        try:
-            if self._io.headers is None:
-                self._io.get("me")
-        finally:
-            return self._io.headers.rate_limit  # pylint: disable=lost-exception
+        return self._io.headers.rate_limit
 
     @property
     def transaction_remaining(self):
@@ -72,11 +64,8 @@ class API(object):  # pylint: disable=too-few-public-methods
 
         :rtype: :class:`int`
         """
-        try:
-            if self._io.headers is None:
-                self._io.get("me")
-        finally:
-            return self._io.headers.rate_remaining  # pylint: disable=lost-exception
+        return self._io.headers.rate_remaining
+
 
 if __name__ == "__main__":
     pass
diff --git a/src/friendlypins/utils/rest_io.py b/src/friendlypins/utils/rest_io.py
index 4fe03f1..50c9136 100644
--- a/src/friendlypins/utils/rest_io.py
+++ b/src/friendlypins/utils/rest_io.py
@@ -35,6 +35,12 @@ class RestIO(object):
 
         :rtype: :class:`friendlypins.headers.Headers`
         """
+        if not self._latest_header:
+            temp_url = "{0}/me".format(self._root_url)
+            properties = {"access_token": self._token}
+            response = requests.get(temp_url, params=properties)
+            self._latest_header = Headers(response.headers)
+            response.raise_for_status()
         return self._latest_header
 
     def get(self, path, properties=None):
diff --git a/tox.ini b/tox.ini
index e5c3570..1fc289c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -13,7 +13,7 @@ commands =
     /bin/bash -c 'pip install dist/*.whl'
     pylint setup.py
     bash -c "source toxenv.sh; pylint ./src/$PROJECT_NAME"
-    bash -c "source toxenv.sh; pytest ./tests -v --cov-report html --cov $PROJECT_NAME --no-cov-on-fail"
+    bash -c "source toxenv.sh; pytest {posargs} ./tests -v --cov-report html --cov $PROJECT_NAME --no-cov-on-fail"
 
 [testenv:py2]
 deps = -rtests/python2.reqs


Test patch:
diff --git a/tests/test_api.py b/tests/test_api.py
index 552f578..026b261 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -1,4 +1,3 @@
-import pytest
 import mock
 from friendlypins.api import API
 from dateutil import tz
@@ -51,13 +50,13 @@ def test_transaction_limit(mock_requests):
       'Pinterest-Generated-By': '',
     }
     mock_requests.get.return_value = mock_response
-    mock_response.raise_for_status.side_effect = Exception
 
     obj = API("abcd1234")
-    obj.transaction_limit == expected_rate_limit
+    assert obj.transaction_limit == expected_rate_limit
     mock_requests.get.assert_called_once()
     mock_response.raise_for_status.assert_called_once()
 
+
 @mock.patch("friendlypins.utils.rest_io.requests")
 def test_transaction_remaining(mock_requests):
     mock_response = mock.MagicMock()
@@ -78,13 +77,13 @@ def test_transaction_remaining(mock_requests):
       'Pinterest-Generated-By': '',
     }
     mock_requests.get.return_value = mock_response
-    mock_response.raise_for_status.side_effect = Exception
 
     obj = API("abcd1234")
     tmp = obj.transaction_remaining
     mock_requests.get.assert_called_once()
     mock_response.raise_for_status.assert_called_once()
-    tmp == expected_rate_remaining
+    assert tmp == expected_rate_remaining
+
 
 @mock.patch("friendlypins.utils.rest_io.requests")
 def test_rate_refresh(mock_requests):
@@ -109,7 +108,6 @@ def test_rate_refresh(mock_requests):
       'X-Ratelimit-Refresh': str(refresh_time)
     }
     mock_requests.get.return_value = mock_response
-    mock_response.raise_for_status.side_effect = Exception
 
     obj = API("abcd1234")
     tmp = obj.rate_limit_refresh
@@ -118,7 +116,3 @@ def test_rate_refresh(mock_requests):
     mock_response.raise_for_status.assert_called_once()
     tmp = tmp.astimezone(tz.tzutc())
     assert tmp.strftime("%a, %d %b %Y %H:%M:%S") == expected_time_str
-
-
-if __name__ == "__main__":
-    pytest.main([__file__, "-v", "-s"])
diff --git a/tests/test_board.py b/tests/test_board.py
index 698aed6..94753aa 100644
--- a/tests/test_board.py
+++ b/tests/test_board.py
@@ -1,7 +1,7 @@
-import pytest
 import mock
 from friendlypins.board import Board
 
+
 def test_board_properties():
     expected_id = 1234
     expected_name = "MyBoard"
@@ -64,6 +64,7 @@ def test_get_pins():
     assert expected_id == result[0].unique_id
     assert expected_mediatype == result[0].media_type
 
+
 def test_delete():
     data = {
         "id": "12345678",
@@ -75,6 +76,3 @@ def test_delete():
     obj.delete()
 
     mock_io.delete.assert_called_once()
-
-if __name__ == "__main__":
-    pytest.main([__file__, "-v", "-s"])
diff --git a/tests/test_console_actions.py b/tests/test_console_actions.py
index eb41b05..3097767 100644
--- a/tests/test_console_actions.py
+++ b/tests/test_console_actions.py
@@ -1,4 +1,3 @@
-import pytest
 import mock
 import os
 from friendlypins.utils.console_actions import download_thumbnails, \
@@ -6,6 +5,7 @@ from friendlypins.utils.console_actions import download_thumbnails, \
 import friendlypins.utils.console_actions as ca
 ca.DISABLE_PROGRESS_BARS = True
 
+
 @mock.patch("friendlypins.utils.console_actions.os")
 @mock.patch("friendlypins.utils.console_actions.open")
 @mock.patch("friendlypins.utils.console_actions.requests")
@@ -170,6 +170,7 @@ def test_download_thumbnails_error(rest_io, action_requests, mock_open, mock_os)
     mock_os.path.exists.assert_called()
     assert not mock_open.called
 
+
 @mock.patch("friendlypins.utils.console_actions.os")
 @mock.patch("friendlypins.utils.console_actions.open")
 @mock.patch("friendlypins.utils.console_actions.requests")
@@ -249,6 +250,7 @@ def test_download_thumbnails_missing_board(rest_io, action_requests, mock_open,
     assert not mock_os.path.exists.called
     assert not mock_open.called
 
+
 @mock.patch("friendlypins.utils.console_actions.os")
 @mock.patch("friendlypins.utils.console_actions.open")
 @mock.patch("friendlypins.utils.console_actions.requests")
@@ -332,6 +334,7 @@ def test_download_thumbnails_exists(rest_io, action_requests, mock_open, mock_os
     mock_os.path.exists.assert_called_with(os.path.join(output_folder, expected_filename))
     assert not mock_open.called
 
+
 @mock.patch("friendlypins.api.RestIO")
 def test_delete_board(rest_io):
 
@@ -423,6 +426,7 @@ def test_delete_missing_board(rest_io):
     assert result != 0
     mock_response.delete.assert_not_called()
 
+
 @mock.patch("friendlypins.api.RestIO")
 def test_create_board(rest_io):
     # Fake user data for the user authenticating to Pinterest
@@ -453,6 +457,3 @@ def test_create_board(rest_io):
 
     assert res == 0
     mock_response.get.assert_called_once()
-
-if __name__ == "__main__":
-    pytest.main([__file__, "-v", "-s"])
\ No newline at end of file
diff --git a/tests/test_headers.py b/tests/test_headers.py
index 797e786..f8ea695 100644
--- a/tests/test_headers.py
+++ b/tests/test_headers.py
@@ -1,5 +1,3 @@
-import pytest
-import mock
 from friendlypins.headers import Headers
 from dateutil import tz
 
@@ -32,26 +30,31 @@ def test_get_date_locale():
 
     assert obj.date.tzinfo == tz.tzlocal()
 
+
 def test_get_rate_limit():
     obj = Headers(sample_header)
 
     assert obj.rate_limit == sample_rate_limit
 
+
 def test_get_rate_max():
     obj = Headers(sample_header)
 
     assert obj.rate_remaining == sample_rate_max
 
+
 def test_get_rate_percent():
     obj = Headers(sample_header)
 
     assert obj.percent_rate_remaining == 75
 
+
 def test_get_num_bytes():
     obj = Headers(sample_header)
 
     assert obj.bytes == sample_content_length
 
+
 def test_time_to_refresh():
 
     obj = Headers(sample_header)
@@ -60,5 +63,3 @@ def test_time_to_refresh():
 
     tmp = tmp.astimezone(tz.tzutc())
     assert tmp.strftime("%a, %d %b %Y %H:%M:%S") == expected_time_str
-if __name__ == "__main__":
-    pytest.main([__file__, "-v", "-s"])
\ No newline at end of file
diff --git a/tests/test_pin.py b/tests/test_pin.py
index 7fd8924..0d79543 100644
--- a/tests/test_pin.py
+++ b/tests/test_pin.py
@@ -1,7 +1,7 @@
-import pytest
 import mock
 from friendlypins.pin import Pin
 
+
 def test_pin_properties():
     expected_id = 1234
     expected_note = "Here's my note"
@@ -78,7 +78,3 @@ def test_get_thumbnail():
     result = obj.thumbnail
 
     assert result.url == expected_url
-
-
-if __name__ == "__main__":
-    pytest.main([__file__, "-v", "-s"])
\ No newline at end of file
diff --git a/tests/test_rest_io.py b/tests/test_rest_io.py
index 83b04a5..1539768 100644
--- a/tests/test_rest_io.py
+++ b/tests/test_rest_io.py
@@ -1,7 +1,7 @@
-import pytest
 import mock
 from friendlypins.utils.rest_io import RestIO
 
+
 @mock.patch("friendlypins.utils.rest_io.requests")
 def test_get_method(mock_requests):
     expected_token = "1234abcd"
@@ -23,6 +23,7 @@ def test_get_method(mock_requests):
     assert "access_token" in mock_requests.get.call_args[1]['params']
     assert mock_requests.get.call_args[1]['params']['access_token'] == expected_token
 
+
 @mock.patch("friendlypins.utils.rest_io.requests")
 def test_get_pages_one_page(mock_requests):
     expected_token = "1234abcd"
@@ -47,10 +48,10 @@ def test_get_pages_one_page(mock_requests):
     assert "access_token" in mock_requests.get.call_args[1]['params']
     assert mock_requests.get.call_args[1]['params']['access_token'] == expected_token
 
+
 @mock.patch("friendlypins.utils.rest_io.requests")
 def test_get_headers(mock_requests):
     obj = RestIO("1234abcd")
-    assert obj.headers is None
 
     expected_bytes = 1024
     mock_response = mock.MagicMock()
@@ -59,12 +60,28 @@ def test_get_headers(mock_requests):
     }
     mock_requests.get.return_value = mock_response
     obj.get("me/boards")
+    tmp = obj.headers
 
     mock_requests.get.assert_called_once()
+    assert tmp is not None
+    assert tmp.bytes == expected_bytes
+
+
+@mock.patch("friendlypins.utils.rest_io.requests")
+def test_get_default_headers(mock_requests):
+    obj = RestIO("1234abcd")
+
+    expected_bytes = 1024
+    mock_response = mock.MagicMock()
+    mock_response.headers = {
+        "Content-Length": str(expected_bytes)
+    }
+    mock_requests.get.return_value = mock_response
 
     tmp = obj.headers
     assert tmp is not None
     assert tmp.bytes == expected_bytes
+    mock_requests.get.assert_called_once()
 
 
 @mock.patch("friendlypins.utils.rest_io.requests")
@@ -91,6 +108,4 @@ def test_post(mock_requests):
     assert expected_path in mock_requests.post.call_args[0][0]
     assert "data" in mock_requests.post.call_args[1]
     assert mock_requests.post.call_args[1]["data"] == expected_data
-
-if __name__ == "__main__":
-    pytest.main([__file__, "-v", "-s"])
+    assert res == expected_results
diff --git a/tests/test_thumbnail.py b/tests/test_thumbnail.py
index b9dd141..c43aee6 100644
--- a/tests/test_thumbnail.py
+++ b/tests/test_thumbnail.py
@@ -1,7 +1,6 @@
-import pytest
-import mock
 from friendlypins.thumbnail import Thumbnail
 
+
 def test_thumbnail_properties():
     expected_url = "https://i.pinimg.com/originals/1/2/3/abcd.jpg"
     expected_width = 800
@@ -19,6 +18,3 @@ def test_thumbnail_properties():
     assert obj.url == expected_url
     assert obj.width == expected_width
     assert obj.height == expected_height
-
-if __name__ == "__main__":
-    pytest.main([__file__, "-v", "-s"])
\ No newline at end of file
diff --git a/tests/test_user.py b/tests/test_user.py
index 75d051e..9659aac 100644
--- a/tests/test_user.py
+++ b/tests/test_user.py
@@ -1,7 +1,7 @@
-import pytest
 import mock
 from friendlypins.user import User
 
+
 def test_user_properties():
     expected_url = 'https://www.pinterest.com/MyUserName/'
     expected_firstname = "John"
@@ -63,6 +63,7 @@ def test_get_boards():
     assert expected_name == result[0].name
     assert expected_id == result[0].unique_id
 
+
 def test_create_board():
     expected_name = "My Board"
     expected_desc = "My new board is about this stuff..."
@@ -86,6 +87,3 @@ def test_create_board():
     assert board is not None
     assert board.name == expected_name
     assert board.description == expected_desc
-
-if __name__ == "__main__":
-    pytest.main([__file__, "-v", "-s"])