Repository: TheFriendlyCoder/friendlypins

Issue:
extract common API logic into a helper class
We have several objects in the API that encapsulate primitives from Pinterest. Most of these objects make calls to the Pinterest API to perform actions. Further, there is now some common logic - like accessing paged result sets - that is duplicated across the classes. This duplication should be eliminated by introducing a new helper class which interacts with the Pinterest API, abstracting out the low level details of those requests/responses.

Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index edbaf19..d76012a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,4 +22,5 @@ venv*/
 **/*.orig
 
 # project files
-.vscode/
\ No newline at end of file
+.vscode/
+.idea/
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..a55e7a1
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+  <state>
+    <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
+  </state>
+</component>
\ No newline at end of file
diff --git a/docs/friendlypins.utils.rest_io.rst b/docs/friendlypins.utils.rest_io.rst
new file mode 100644
index 0000000..c29f06c
--- /dev/null
+++ b/docs/friendlypins.utils.rest_io.rst
@@ -0,0 +1,7 @@
+friendlypins.utils.rest\_io module
+==================================
+
+.. automodule:: friendlypins.utils.rest_io
+    :members:
+    :undoc-members:
+    :show-inheritance:
diff --git a/docs/friendlypins.utils.rst b/docs/friendlypins.utils.rst
index f3a2e9a..300853a 100644
--- a/docs/friendlypins.utils.rst
+++ b/docs/friendlypins.utils.rst
@@ -7,6 +7,7 @@ Submodules
 .. toctree::
 
    friendlypins.utils.console_actions
+   friendlypins.utils.rest_io
 
 Module contents
 ---------------
diff --git a/src/friendlypins/api.py b/src/friendlypins/api.py
index ee3b00a..cff46b6 100644
--- a/src/friendlypins/api.py
+++ b/src/friendlypins/api.py
@@ -1,9 +1,9 @@
 """Primary entry point for the Friendly Pinterest library"""
 from __future__ import print_function
 import logging
-import requests
 from friendlypins.user import User
-from friendlypins.headers import Headers
+from friendlypins.utils.rest_io import RestIO
+
 
 class API(object):  # pylint: disable=too-few-public-methods
     """High level abstraction for the core Pinterest API
@@ -13,15 +13,12 @@ class API(object):  # pylint: disable=too-few-public-methods
             Pinterest data
     """
 
-    # URL of the root namespace for the Pinterest API
-    _root_url = 'https://api.pinterest.com/v1'
-
     def __init__(self, personal_access_token):
         self._log = logging.getLogger(__name__)
-        self._token = personal_access_token
+        self._io = RestIO(personal_access_token)
 
     def get_user(self, username=None):
-        """Gets all primitives associated with a particular Pinterst user
+        """Gets all primitives associated with a particular Pinterest user
 
         :param str username:
             Optional name of a user to look up
@@ -34,23 +31,13 @@ class API(object):  # pylint: disable=too-few-public-methods
         if username:
             raise NotImplementedError(
                 "Querying arbitrary Pinerest users is not yet supported.")
-        else:
-            temp_url = "{0}/me".format(self._root_url)
-        temp_url += "?access_token={0}".format(self._token)
-        temp_url += "&fields=id,username,first_name,last_name,bio,created_at,counts,image"
-        response = requests.get(temp_url)
-        response.raise_for_status()
-
-        header = Headers(response.headers)
-        self._log.debug("Getting user query response: %s", header)
-
-        raw = response.json()
 
-        assert 'data' in raw
+        fields = "id,username,first_name,last_name,bio,created_at,counts,image"
+        result = self._io.get("me", {"fields": fields})
+        assert 'data' in result
 
-        return User(raw['data'], self._root_url, self._token)
+        return User(result['data'], self._io)
 
 
-# pylint: disable-all
 if __name__ == "__main__":
     pass
diff --git a/src/friendlypins/board.py b/src/friendlypins/board.py
index 194d50a..045655f 100644
--- a/src/friendlypins/board.py
+++ b/src/friendlypins/board.py
@@ -1,8 +1,6 @@
 """Primitives for interacting with Pinterest boards"""
 import logging
 import json
-import requests
-from friendlypins.headers import Headers
 from friendlypins.pin import Pin
 
 
@@ -10,15 +8,14 @@ class Board(object):
     """Abstraction around a Pinterest board
 
     :param dict data: Raw Pinterest API data describing the board
-    :param str root_url: URL of the Pinterest REST API
-    :param str token: Authentication token for the REST API
+    :param rest_io: reference to the Pinterest REST API
+    :type rest_io: :class:`friendlypins.utils.rest_io.RestIO`
     """
 
-    def __init__(self, data, root_url, token):
+    def __init__(self, data, rest_io):
         self._log = logging.getLogger(__name__)
         self._data = data
-        self._root_url = root_url
-        self._token = token
+        self._io = rest_io
 
     def __str__(self):
         """String representation of this board, for debugging purposes
@@ -76,37 +73,40 @@ class Board(object):
         :rtype: :class:`list` of :class:`friendlypins.pin.Pin`
         """
         self._log.debug('Gettings all pins for board %s...', self.name)
-
-        temp_url = '{0}/boards/{1}/pins/'.format(self._root_url, self.unique_id)
-        temp_url += "?access_token={0}".format(self._token)
-        temp_url += "&limit=100"
-        temp_url += "&fields=id,link,url,creator,board,created_at,note,color"
-        temp_url += ",counts,media,attribution,image,metadata,original_link"
-        response = requests.get(temp_url)
-        response.raise_for_status()
-        retval = []
-        header = Headers(response.headers)
-        self._log.debug("Pins query response header %s", header)
+        retval = list()
+
+        properties = {
+            "fields": ','.join([
+                "id",
+                "link",
+                "url",
+                "creator",
+                "board",
+                "created_at",
+                "note,color",
+                "counts",
+                "media",
+                "attribution",
+                "image",
+                "metadata",
+                "original_link"
+            ])
+        }
 
         while True:
-            raw = response.json()
-            assert 'data' in raw
-
-            for cur_item in raw['data']:
-                retval.append(Pin(cur_item, self._root_url, self._token))
-
-            self._log.debug("Raw keys are %s", raw.keys())
-            self._log.debug("Paged info is %s", raw['page'])
-            if not raw['page']['cursor']:
+            result = self._io.get(
+                "boards/{0}/pins".format(self.unique_id),
+                properties)
+            assert 'data' in result
+
+            for cur_item in result['data']:
+                retval.append(Pin(cur_item, self._io))
+            if not result["page"]["cursor"]:
                 break
-
-            paged_url = temp_url + "&cursor={0}".format(raw['page']['cursor'])
-            response = requests.get(paged_url)
-            response.raise_for_status()
-            header = Headers(response.headers)
-            self._log.debug("Pins query response header %s", header)
+            properties["cursor"] = result["page"]["cursor"]
 
         return retval
 
+
 if __name__ == "__main__":
     pass
diff --git a/src/friendlypins/headers.py b/src/friendlypins/headers.py
index b05b9f8..7fc1552 100644
--- a/src/friendlypins/headers.py
+++ b/src/friendlypins/headers.py
@@ -3,6 +3,7 @@ from datetime import datetime
 import json
 from dateutil import tz
 
+
 class Headers(object):
     """Abstraction around the Pinterest API HTTP response header
 
@@ -87,5 +88,6 @@ class Headers(object):
         """
         return int(self._data['Content-Length'])
 
+
 if __name__ == "__main__":
     pass
diff --git a/src/friendlypins/pin.py b/src/friendlypins/pin.py
index 9faa11b..f727468 100644
--- a/src/friendlypins/pin.py
+++ b/src/friendlypins/pin.py
@@ -1,8 +1,6 @@
 """Primitives for operating on Pinterest pins"""
 import logging
 import json
-import requests
-from friendlypins.headers import Headers
 from friendlypins.thumbnail import Thumbnail
 
 
@@ -10,14 +8,13 @@ class Pin(object):
     """Abstraction around a Pinterest pin
 
     :param dict data: Raw Pinterest API data describing a pin
-    :param str root_url: URL of the Pinterest REST API
-    :param str token: Authentication token for interacting with the API
+    :param rest_io: reference to the Pinterest REST API
+    :type rest_io: :class:`friendlypins.utils.rest_io.RestIO`
     """
 
-    def __init__(self, data, root_url, token):
+    def __init__(self, data, rest_io):
         self._log = logging.getLogger(__name__)
-        self._root_url = root_url
-        self._token = token
+        self._io = rest_io
         self._data = data
 
     def __str__(self):
@@ -92,17 +89,8 @@ class Pin(object):
     def delete(self):
         """Removes this pin from it's respective board"""
         self._log.debug('Deleting pin %s', repr(self))
-        temp_url = '{0}/pins/{1}/'.format(
-            self._root_url,
-            self.unique_id)
-        temp_url += "?access_token={0}".format(self._token)
+        self._io.delete('pins/{0}'.format(self.unique_id))
 
-        response = requests.delete(temp_url)
-
-        header = Headers(response.headers)
-        self._log.debug("Boards query response header %s", header)
-
-        response.raise_for_status()
 
 if __name__ == "__main__":
     pass
diff --git a/src/friendlypins/scripts/fpins.py b/src/friendlypins/scripts/fpins.py
index 0115b46..1fe03d5 100644
--- a/src/friendlypins/scripts/fpins.py
+++ b/src/friendlypins/scripts/fpins.py
@@ -5,6 +5,7 @@ import shlex
 import sys
 from friendlypins.utils.console_actions import download_thumbnails
 
+
 def _download_thumbnails(args):
     """Callback for performing the thumbnail download operation
 
@@ -141,5 +142,6 @@ def main(args=None):
         log.debug("Details: ", exc_info=True)
         return -1
 
+
 if __name__ == "__main__":
     pass
diff --git a/src/friendlypins/thumbnail.py b/src/friendlypins/thumbnail.py
index 58bc810..bf5cc5f 100644
--- a/src/friendlypins/thumbnail.py
+++ b/src/friendlypins/thumbnail.py
@@ -56,5 +56,6 @@ class Thumbnail(object):
         """
         return self._data['original']['url']
 
+
 if __name__ == "__main__":
     pass
diff --git a/src/friendlypins/user.py b/src/friendlypins/user.py
index 3121d1d..de3306d 100644
--- a/src/friendlypins/user.py
+++ b/src/friendlypins/user.py
@@ -1,24 +1,21 @@
 """Interfaces for interacting with Pinterest users"""
 import logging
 import json
-import requests
 from friendlypins.board import Board
-from friendlypins.headers import Headers
 
 
 class User(object):
     """Abstraction around a Pinterest user and their associated data
 
     :param dict data: JSON data parsed from the API
-    :param str root_url: URL of the Pinterest REST API
-    :param str token: Authentication token for interacting with the API
+    :param rest_io: reference to the Pinterest REST API
+    :type rest_io: :class:`friendlypins.utils.rest_io.RestIO`
     """
 
-    def __init__(self, data, root_url, token):
+    def __init__(self, data, rest_io):
         self._log = logging.getLogger(__name__)
         self._data = data
-        self._root_url = root_url
-        self._token = token
+        self._io = rest_io
 
     def __str__(self):
         """String representation of this user, for debugging purposes
@@ -103,22 +100,16 @@ class User(object):
         """
         self._log.debug("Loading boards for user %s...", self.name)
 
-        temp_url = '{0}/me/boards/'.format(self._root_url)
-        temp_url += "?access_token={0}".format(self._token)
-        temp_url += "&fields=id,name,url,description,creator,created_at,counts,image"
-        response = requests.get(temp_url)
+        fields = "id,name,url,description,creator,created_at,counts,image"
+        result = self._io.get('me/boards', {"fields": fields})
 
-        header = Headers(response.headers)
-        self._log.debug("Boards query response header %s", header)
-
-        response.raise_for_status()
-        raw = response.json()
-        assert 'data' in raw
+        assert 'data' in result
 
         retval = []
-        for cur_item in raw['data']:
-            retval.append(Board(cur_item, self._root_url, self._token))
+        for cur_item in result['data']:
+            retval.append(Board(cur_item, self._io))
         return retval
 
+
 if __name__ == "__main__":
     pass
diff --git a/src/friendlypins/utils/console_actions.py b/src/friendlypins/utils/console_actions.py
index 864bfbc..1ca66ef 100644
--- a/src/friendlypins/utils/console_actions.py
+++ b/src/friendlypins/utils/console_actions.py
@@ -7,6 +7,7 @@ from tqdm import tqdm
 from friendlypins.api import API
 from friendlypins.headers import Headers
 
+
 def _download_pin(pin, folder):
     """Helper method for downloading a thumbnail from a single pin
 
@@ -44,6 +45,7 @@ def _download_pin(pin, folder):
         return 2
     return 0
 
+
 def download_thumbnails(api_token, board_name, output_folder, delete):
     """Downloads thumbnails of all pins on a board
 
@@ -85,5 +87,6 @@ def download_thumbnails(api_token, board_name, output_folder, delete):
 
     return 0
 
+
 if __name__ == "__main__":
     pass
diff --git a/src/friendlypins/utils/rest_io.py b/src/friendlypins/utils/rest_io.py
new file mode 100644
index 0000000..61d1aae
--- /dev/null
+++ b/src/friendlypins/utils/rest_io.py
@@ -0,0 +1,81 @@
+"""Abstraction around the raw Pinterest REST API calls"""
+import logging
+import requests
+from friendlypins.headers import Headers
+
+
+class RestIO(object):
+    """Interface for low level REST API interactions
+
+    :param str authentication_token:
+        Personal API token for authenticating to REST API
+    """
+
+    # URL of the root namespace for the Pinterest API
+    _root_url = 'https://api.pinterest.com/v1'
+
+    def __init__(self, authentication_token):
+        self._log = logging.getLogger(__name__)
+        self._token = authentication_token
+        self._latest_header = None
+
+    @property
+    def root_url(self):
+        """Gets root url"""
+        return self._root_url
+
+    @property
+    def token(self):
+        """Gets API token"""
+        return self._token
+
+    def get(self, path, properties=None):
+        """Gets API data from a given sub-path
+
+        :param str path: sub-path with in the REST API to query
+        :param dict properties:
+            optional set of request properties to append to the API call
+        :returns: json data returned from the API endpoint
+        :rtype: :class:`dict`
+        """
+        self._log.debug(
+            "Getting data from %s with options %s",
+            path,
+            properties
+        )
+        temp_url = "{0}/{1}".format(self._root_url, path)
+
+        if properties is None:
+            properties = dict()
+        properties["limit"] = "100"
+        properties["access_token"] = self._token
+
+        response = requests.get(temp_url, params=properties)
+        response.raise_for_status()
+
+        self._latest_header = Headers(response.headers)
+        self._log.debug("%s query header: %s", path, self._latest_header)
+
+        return response.json()
+
+    def delete(self, path):
+        """Sends a delete request to a remote endpoint
+
+        :param str path: API endpoint to send delete request to"""
+        temp_url = '{0}/{1}'.format(
+            self._root_url,
+            path)
+
+        properties = {
+            "access_token": self._token
+        }
+
+        response = requests.delete(temp_url, params=properties)
+        response.raise_for_status()
+        header = Headers(response.headers)
+        self._log.debug("Headers for delete on %s are: %s", path, header)
+        self._log.debug("Response from delete was %s", response.text)
+
+
+if __name__ == "__main__":
+    pass


Test patch:
diff --git a/unit_tests/test_api.py b/unit_tests/test_api.py
index 0a6e5c1..b013c9e 100644
--- a/unit_tests/test_api.py
+++ b/unit_tests/test_api.py
@@ -3,7 +3,6 @@ import mock
 from friendlypins.api import API
 
 def test_get_user():
-    obj = API('abcd1234')
     expected_url = 'https://www.pinterest.com/MyUserName/'
     expected_firstname = "John"
     expected_lastname = "Doe"
@@ -16,10 +15,12 @@ def test_get_user():
             'id': str(expected_id)
         }
     }
-    with mock.patch("friendlypins.api.requests") as mock_requests:
-        mock_response = mock.MagicMock()
-        mock_response.json.return_value = expected_data
-        mock_requests.get.return_value = mock_response
+    with mock.patch("friendlypins.api.RestIO") as mock_io:
+        mock_obj = mock.MagicMock()
+        mock_obj.get.return_value = expected_data
+        mock_io.return_value = mock_obj
+
+        obj = API('abcd1234')
         result = obj.get_user()
 
         assert expected_url == result.url
@@ -27,8 +28,6 @@ def test_get_user():
         assert expected_lastname == result.last_name
         assert expected_id == result.unique_id
 
-        mock_response.raise_for_status.assert_called_once()
-
 
 if __name__ == "__main__":
     pytest.main([__file__, "-v", "-s"])
diff --git a/unit_tests/test_board.py b/unit_tests/test_board.py
index e2eafe1..5daac85 100644
--- a/unit_tests/test_board.py
+++ b/unit_tests/test_board.py
@@ -16,20 +16,19 @@ def test_board_properties():
         }
     }
 
-    obj = Board(sample_data, 'http://pinterest_url', '1234abcd')
+    mock_io = mock.MagicMock()
+    obj = Board(sample_data, mock_io)
     assert obj.unique_id == expected_id
     assert obj.name == expected_name
     assert obj.url == expected_url
     assert obj.num_pins == expected_pin_count
 
+
 def test_get_all_pins():
     data = {
         'id': '987654321',
         'name': 'MyBoard'
     }
-    api_url = "https://pinterest_url/v1"
-    token = "1234abcd"
-    obj = Board(data, api_url, token)
 
     expected_id = 1234
     expected_url = "https://www.pinterest.ca/MyName/MyPin/"
@@ -51,19 +50,18 @@ def test_get_all_pins():
         }
     }
 
-    with mock.patch("friendlypins.board.requests") as mock_requests:
-        mock_response = mock.MagicMock()
-        mock_response.json.return_value = expected_data
-        mock_requests.get.return_value = mock_response
-        result = obj.all_pins
+    mock_io = mock.MagicMock()
+    mock_io.get.return_value = expected_data
+    obj = Board(data, mock_io)
+
+    result = obj.all_pins
 
-        assert len(result) == 1
-        assert expected_url == result[0].url
-        assert expected_note == result[0].note
-        assert expected_id == result[0].unique_id
-        assert expected_mediatype == result[0].media_type
+    assert len(result) == 1
+    assert expected_url == result[0].url
+    assert expected_note == result[0].note
+    assert expected_id == result[0].unique_id
+    assert expected_mediatype == result[0].media_type
 
-        mock_response.raise_for_status.assert_called_once()
 
 if __name__ == "__main__":
     pytest.main([__file__, "-v", "-s"])
diff --git a/unit_tests/test_console_actions.py b/unit_tests/test_console_actions.py
index b91cd0d..170f224 100644
--- a/unit_tests/test_console_actions.py
+++ b/unit_tests/test_console_actions.py
@@ -7,10 +7,8 @@ from friendlypins.utils.console_actions import download_thumbnails
 @mock.patch("friendlypins.utils.console_actions.os")
 @mock.patch("friendlypins.utils.console_actions.open")
 @mock.patch("friendlypins.utils.console_actions.requests")
-@mock.patch("friendlypins.board.requests")
-@mock.patch("friendlypins.user.requests")
-@mock.patch("friendlypins.api.requests")
-def test_download_thumbnails(api_requests, user_requests, board_requests, action_requests, mock_open, mock_os):
+@mock.patch("friendlypins.api.RestIO")
+def test_download_thumbnails(rest_io, action_requests, mock_open, mock_os):
 
     # Fake user data for the user authenticating to Pinterest
     expected_user_data = {
@@ -60,17 +58,13 @@ def test_download_thumbnails(api_requests, user_requests, board_requests, action
     }
 
     # fake our Pinterest API data to flex our implementation logic
-    mock_user_response = mock.MagicMock()
-    mock_user_response.json.return_value = expected_user_data
-    api_requests.get.return_value = mock_user_response
-
-    mock_board_response = mock.MagicMock()
-    mock_board_response.json.return_value = expected_board_data
-    user_requests.get.return_value = mock_board_response
-
-    mock_pin_response = mock.MagicMock()
-    mock_pin_response.json.return_value = expected_pin_data
-    board_requests.get.return_value = mock_pin_response
+    mock_response = mock.MagicMock()
+    mock_response.get.side_effect = [
+        expected_user_data,
+        expected_board_data,
+        expected_pin_data
+        ]
+    rest_io.return_value = mock_response
 
     # Make sure the code think's the output file where the
     # thumbnail is to be downloaded doesn't already exist
@@ -91,11 +85,8 @@ def test_download_thumbnails(api_requests, user_requests, board_requests, action
 @mock.patch("friendlypins.utils.console_actions.os")
 @mock.patch("friendlypins.utils.console_actions.open")
 @mock.patch("friendlypins.utils.console_actions.requests")
-@mock.patch("friendlypins.board.requests")
-@mock.patch("friendlypins.user.requests")
-@mock.patch("friendlypins.api.requests")
-@mock.patch("friendlypins.pin.requests")
-def test_download_thumbnails_with_delete(pin_requests, api_requests, user_requests, board_requests, action_requests, mock_open, mock_os):
+@mock.patch("friendlypins.api.RestIO")
+def test_download_thumbnails_with_delete(rest_io, action_requests, mock_open, mock_os):
 
     # Fake user data for the user authenticating to Pinterest
     expected_user_data = {
@@ -145,20 +136,13 @@ def test_download_thumbnails_with_delete(pin_requests, api_requests, user_reques
     }
 
     # fake our Pinterest API data to flex our implementation logic
-    mock_user_response = mock.MagicMock()
-    mock_user_response.json.return_value = expected_user_data
-    api_requests.get.return_value = mock_user_response
-
-    mock_board_response = mock.MagicMock()
-    mock_board_response.json.return_value = expected_board_data
-    user_requests.get.return_value = mock_board_response
-
-    mock_pin_response = mock.MagicMock()
-    mock_pin_response.json.return_value = expected_pin_data
-    board_requests.get.return_value = mock_pin_response
-
-    mock_delete_response = mock.MagicMock()
-    pin_requests.delete.return_value = mock_delete_response
+    mock_response = mock.MagicMock()
+    mock_response.get.side_effect = [
+        expected_user_data,
+        expected_board_data,
+        expected_pin_data
+        ]
+    rest_io.return_value = mock_response
 
     # Make sure the code think's the output file where the
     # thumbnail is to be downloaded doesn't already exist
@@ -174,15 +158,13 @@ def test_download_thumbnails_with_delete(pin_requests, api_requests, user_reques
     mock_os.makedirs.assert_called()
     mock_os.path.exists.assert_called()
     mock_open.assert_called()
-    pin_requests.delete.assert_called_once()
+    mock_response.delete.assert_called_once()
 
 @mock.patch("friendlypins.utils.console_actions.os")
 @mock.patch("friendlypins.utils.console_actions.open")
 @mock.patch("friendlypins.utils.console_actions.requests")
-@mock.patch("friendlypins.board.requests")
-@mock.patch("friendlypins.user.requests")
-@mock.patch("friendlypins.api.requests")
-def test_download_thumbnails_error(api_requests, user_requests, board_requests, action_requests, mock_open, mock_os):
+@mock.patch("friendlypins.api.RestIO")
+def test_download_thumbnails_error(rest_io, action_requests, mock_open, mock_os):
 
     # Fake user data for the user authenticating to Pinterest
     expected_user_data = {
@@ -232,17 +214,13 @@ def test_download_thumbnails_error(api_requests, user_requests, board_requests,
     }
 
     # fake our Pinterest API data to flex our implementation logic
-    mock_user_response = mock.MagicMock()
-    mock_user_response.json.return_value = expected_user_data
-    api_requests.get.return_value = mock_user_response
-
-    mock_board_response = mock.MagicMock()
-    mock_board_response.json.return_value = expected_board_data
-    user_requests.get.return_value = mock_board_response
-
-    mock_pin_response = mock.MagicMock()
-    mock_pin_response.json.return_value = expected_pin_data
-    board_requests.get.return_value = mock_pin_response
+    mock_response = mock.MagicMock()
+    mock_response.get.side_effect = [
+        expected_user_data,
+        expected_board_data,
+        expected_pin_data
+        ]
+    rest_io.return_value = mock_response
 
     # Make sure the code think's the output file where the
     # thumbnail is to be downloaded doesn't already exist
@@ -267,10 +245,8 @@ def test_download_thumbnails_error(api_requests, user_requests, board_requests,
 @mock.patch("friendlypins.utils.console_actions.os")
 @mock.patch("friendlypins.utils.console_actions.open")
 @mock.patch("friendlypins.utils.console_actions.requests")
-@mock.patch("friendlypins.board.requests")
-@mock.patch("friendlypins.user.requests")
-@mock.patch("friendlypins.api.requests")
-def test_download_thumbnails_missing_board(api_requests, user_requests, board_requests, action_requests, mock_open, mock_os):
+@mock.patch("friendlypins.api.RestIO")
+def test_download_thumbnails_missing_board(rest_io, action_requests, mock_open, mock_os):
 
     # Fake user data for the user authenticating to Pinterest
     expected_user_data = {
@@ -319,17 +295,13 @@ def test_download_thumbnails_missing_board(api_requests, user_requests, board_re
     }
 
     # fake our Pinterest API data to flex our implementation logic
-    mock_user_response = mock.MagicMock()
-    mock_user_response.json.return_value = expected_user_data
-    api_requests.get.return_value = mock_user_response
-
-    mock_board_response = mock.MagicMock()
-    mock_board_response.json.return_value = expected_board_data
-    user_requests.get.return_value = mock_board_response
-
-    mock_pin_response = mock.MagicMock()
-    mock_pin_response.json.return_value = expected_pin_data
-    board_requests.get.return_value = mock_pin_response
+    mock_response = mock.MagicMock()
+    mock_response.get.side_effect = [
+        expected_user_data,
+        expected_board_data,
+        expected_pin_data
+        ]
+    rest_io.return_value = mock_response
 
     # Make sure the code think's the output file where the
     # thumbnail is to be downloaded doesn't already exist
@@ -349,10 +321,8 @@ def test_download_thumbnails_missing_board(api_requests, user_requests, board_re
 @mock.patch("friendlypins.utils.console_actions.os")
 @mock.patch("friendlypins.utils.console_actions.open")
 @mock.patch("friendlypins.utils.console_actions.requests")
-@mock.patch("friendlypins.board.requests")
-@mock.patch("friendlypins.user.requests")
-@mock.patch("friendlypins.api.requests")
-def test_download_thumbnails_exists(api_requests, user_requests, board_requests, action_requests, mock_open, mock_os):
+@mock.patch("friendlypins.api.RestIO")
+def test_download_thumbnails_exists(rest_io, action_requests, mock_open, mock_os):
 
     # Fake user data for the user authenticating to Pinterest
     expected_user_data = {
@@ -403,17 +373,13 @@ def test_download_thumbnails_exists(api_requests, user_requests, board_requests,
     }
 
     # fake our Pinterest API data to flex our implementation logic
-    mock_user_response = mock.MagicMock()
-    mock_user_response.json.return_value = expected_user_data
-    api_requests.get.return_value = mock_user_response
-
-    mock_board_response = mock.MagicMock()
-    mock_board_response.json.return_value = expected_board_data
-    user_requests.get.return_value = mock_board_response
-
-    mock_pin_response = mock.MagicMock()
-    mock_pin_response.json.return_value = expected_pin_data
-    board_requests.get.return_value = mock_pin_response
+    mock_response = mock.MagicMock()
+    mock_response.get.side_effect = [
+        expected_user_data,
+        expected_board_data,
+        expected_pin_data
+        ]
+    rest_io.return_value = mock_response
 
     # Make sure the code think's the output file where the
     # thumbnail is to be downloaded exists already
diff --git a/unit_tests/test_pin.py b/unit_tests/test_pin.py
index 7d5586c..7fd8924 100644
--- a/unit_tests/test_pin.py
+++ b/unit_tests/test_pin.py
@@ -18,7 +18,8 @@ def test_pin_properties():
         }
     }
 
-    obj = Pin(sample_data, "http://www.pinterest.com", "1234abcd")
+    mock_io = mock.MagicMock()
+    obj = Pin(sample_data, mock_io)
 
     assert obj.unique_id == expected_id
     assert obj.note == expected_note
@@ -26,6 +27,7 @@ def test_pin_properties():
     assert obj.link == expected_link
     assert obj.media_type == expected_media_type
 
+
 def test_pin_missing_media_type():
     expected_id = 1234
     expected_note = "Here's my note"
@@ -38,7 +40,8 @@ def test_pin_missing_media_type():
         "url": expected_url,
     }
 
-    obj = Pin(sample_data, "http://www.pinterest.com", "1234abcd")
+    mock_io = mock.MagicMock()
+    obj = Pin(sample_data, mock_io)
 
     assert obj.unique_id == expected_id
     assert obj.note == expected_note
@@ -46,24 +49,19 @@ def test_pin_missing_media_type():
     assert obj.link == expected_link
     assert obj.media_type is None
 
-def test_delete():
-    api_url = "https://pinterest_url/v1"
-    token = "1234abcd"
 
+def test_delete():
     data = {
         "id": "12345678",
         "note": "My Pin Description"
     }
 
-    obj = Pin(data, api_url, token)
+    mock_io = mock.MagicMock()
+    obj = Pin(data, mock_io)
+    obj.delete()
 
-    with mock.patch("friendlypins.pin.requests") as mock_requests:
-        mock_response = mock.MagicMock()
-        mock_requests.delete.return_value = mock_response
+    mock_io.delete.assert_called_once()
 
-        obj.delete()
-        mock_requests.delete.assert_called_once()
-        mock_response.raise_for_status.assert_called_once()
 
 def test_get_thumbnail():
     expected_url = "https://i.pinimg.com/r/pin/12345"
@@ -75,9 +73,12 @@ def test_get_thumbnail():
         }
     }
 
-    obj = Pin(data, "http://www.pinterest.com", "1234abcd")
+    mock_io = mock.MagicMock()
+    obj = Pin(data, mock_io)
     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/unit_tests/test_user.py b/unit_tests/test_user.py
index 5850b5b..9184835 100644
--- a/unit_tests/test_user.py
+++ b/unit_tests/test_user.py
@@ -20,7 +20,8 @@ def test_user_properties():
         }
     }
 
-    obj = User(data, "https://pinterest_url/v1", "1234abcd")
+    mock_io = mock.MagicMock()
+    obj = User(data, mock_io)
     assert expected_url == obj.url
     assert expected_firstname == obj.first_name
     assert expected_lastname == obj.last_name
@@ -34,9 +35,6 @@ def test_get_boards():
         "first_name": "John",
         "last_name": "Doe"
     }
-    api_url = "https://pinterest_url/v1"
-    token = "1234abcd"
-    obj = User(data, api_url, token)
 
     expected_id = 1234
     expected_name = "MyBoard"
@@ -49,18 +47,16 @@ def test_get_boards():
         }]
     }
 
-    with mock.patch("friendlypins.user.requests") as mock_requests:
-        mock_response = mock.MagicMock()
-        mock_response.json.return_value = expected_data
-        mock_requests.get.return_value = mock_response
-        result = obj.boards
+    mock_io = mock.MagicMock()
+    mock_io.get.return_value = expected_data
+    obj = User(data, mock_io)
+    result = obj.boards
 
-        assert len(result) == 1
-        assert expected_url == result[0].url
-        assert expected_name == result[0].name
-        assert expected_id == result[0].unique_id
+    assert len(result) == 1
+    assert expected_url == result[0].url
+    assert expected_name == result[0].name
+    assert expected_id == result[0].unique_id
 
-        mock_response.raise_for_status.assert_called_once()
 
 if __name__ == "__main__":
     pytest.main([__file__, "-v", "-s"])