Repository: TheFriendlyCoder/friendlypins

Issue:
Add progress bar support to fpins console app
To make it easier to track the overall progress of a lengthy download operation, we should add support for showing a progress bar to the fpins console app.

Discussion:

Patch:
diff --git a/setup.py b/setup.py
index 6604fe6..7c44b31 100755
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,9 @@ PROJECT_NAME = 'friendlypins'
 PROJECT_DEPENDENCIES = [
     'requests',
     'six',
-    'dateutils']
+    'dateutils',
+    'tqdm',
+    'pillow']
 PROJECT_DEV_DEPENDENCIES = [
     'wheel<1.0.0',
     'twine<2.0.0',
diff --git a/src/friendlypins/headers.py b/src/friendlypins/headers.py
index 5df1ea4..b05b9f8 100644
--- a/src/friendlypins/headers.py
+++ b/src/friendlypins/headers.py
@@ -79,5 +79,13 @@ class Headers(object):
         # return time data in current locale for convenience
         return date_with_tz.astimezone(tz.tzlocal())
 
+    @property
+    def bytes(self):
+        """Gets the number of bytes contained in the response data
+
+        :rtype: :class:`int`
+        """
+        return int(self._data['Content-Length'])
+
 if __name__ == "__main__":
     pass
diff --git a/src/friendlypins/utils/console_actions.py b/src/friendlypins/utils/console_actions.py
index 84c63b7..864bfbc 100644
--- a/src/friendlypins/utils/console_actions.py
+++ b/src/friendlypins/utils/console_actions.py
@@ -3,7 +3,9 @@ import logging
 import os
 from six.moves import urllib
 import requests
+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
@@ -15,6 +17,7 @@ def _download_pin(pin, folder):
     :rtype: :class:`int`
     """
     log = logging.getLogger(__name__)
+
     temp_url = urllib.parse.urlparse(pin.thumbnail.url)
     temp_filename = os.path.basename(temp_url.path)
     output_file = os.path.join(folder, temp_filename)
@@ -28,6 +31,9 @@ def _download_pin(pin, folder):
     try:
         response = requests.get(pin.thumbnail.url, stream=True)
         response.raise_for_status()
+        headers = Headers(response.headers)
+        log.debug(headers)
+
         with open(output_file, "wb") as handle:
             for data in response.iter_content():
                 handle.write(data)
@@ -64,16 +70,18 @@ def download_thumbnails(api_token, board_name, output_folder, delete):
         return 1
 
     all_pins = selected_board.all_pins
-    log.info('Downloading %s thumbnails...', len(all_pins))
+    log.info('Downloading thumbnails...')
     if not os.path.exists(output_folder):
         os.makedirs(output_folder)
 
-    for cur_pin in all_pins:
-        retval = _download_pin(cur_pin, output_folder)
-        if retval:
-            return retval
-        if delete:
-            cur_pin.delete()
+    with tqdm(total=selected_board.num_pins, unit='b', ncols=80) as pbar:
+        for cur_pin in all_pins:
+            retval = _download_pin(cur_pin, output_folder)
+            if retval:
+                return retval
+            if delete:
+                cur_pin.delete()
+            pbar.update()
 
     return 0
 


Test patch:
diff --git a/unit_tests/test_console_actions.py b/unit_tests/test_console_actions.py
index d92a3e9..b91cd0d 100644
--- a/unit_tests/test_console_actions.py
+++ b/unit_tests/test_console_actions.py
@@ -28,7 +28,10 @@ def test_download_thumbnails(api_requests, user_requests, board_requests, action
         "data": [{
             "id": "6789",
             "name": expected_board_name,
-            "url": "https://www.pinterest.ca/MyName/MyBoard/"
+            "url": "https://www.pinterest.ca/MyName/MyBoard/",
+            "counts": {
+                "pins": 1
+            }
         }]
     }
 
@@ -110,7 +113,10 @@ def test_download_thumbnails_with_delete(pin_requests, api_requests, user_reques
         "data": [{
             "id": "6789",
             "name": expected_board_name,
-            "url": "https://www.pinterest.ca/MyName/MyBoard/"
+            "url": "https://www.pinterest.ca/MyName/MyBoard/",
+            "counts": {
+                "pins": 1
+            }
         }]
     }
 
@@ -194,7 +200,10 @@ def test_download_thumbnails_error(api_requests, user_requests, board_requests,
         "data": [{
             "id": "6789",
             "name": expected_board_name,
-            "url": "https://www.pinterest.ca/MyName/MyBoard/"
+            "url": "https://www.pinterest.ca/MyName/MyBoard/",
+            "counts": {
+                "pins": 1
+            }
         }]
     }
 
@@ -278,7 +287,10 @@ def test_download_thumbnails_missing_board(api_requests, user_requests, board_re
         "data": [{
             "id": "6789",
             "name": "MyBoard",
-            "url": "https://www.pinterest.ca/MyName/MyBoard/"
+            "url": "https://www.pinterest.ca/MyName/MyBoard/",
+            "counts": {
+                "pins": 1
+            }
         }]
     }
 
@@ -358,7 +370,10 @@ def test_download_thumbnails_exists(api_requests, user_requests, board_requests,
         "data": [{
             "id": "6789",
             "name": expected_board_name,
-            "url": "https://www.pinterest.ca/MyName/MyBoard/"
+            "url": "https://www.pinterest.ca/MyName/MyBoard/",
+            "counts": {
+                "pins": 1
+            }
         }]
     }
 
diff --git a/unit_tests/test_headers.py b/unit_tests/test_headers.py
index e7bf38f..0a5e77e 100644
--- a/unit_tests/test_headers.py
+++ b/unit_tests/test_headers.py
@@ -3,8 +3,9 @@ import mock
 from friendlypins.headers import Headers
 from dateutil import tz
 
-sample_rate_limit = "200"
-sample_rate_max = "150"
+sample_rate_limit = 200
+sample_rate_max = 150
+sample_content_length = 1024
 sample_header = {
       'Access-Control-Allow-Origin': '*',
       'Age': '0',
@@ -14,12 +15,13 @@ sample_header = {
       'Pinterest-Version': 'e3f92ef',
       'X-Content-Type-Options': 'nosniff',
       'X-Pinterest-RID': '12345678',
-      'X-Ratelimit-Limit': sample_rate_limit,
-      'X-Ratelimit-Remaining': sample_rate_max,
+      'X-Ratelimit-Limit': str(sample_rate_limit),
+      'X-Ratelimit-Remaining': str(sample_rate_max),
       'Transfer-Encoding': 'chunked',
       'Date': 'Sat, 31 Mar 2018 10:58:09 GMT',
       'Connection': 'keep-alive',
-      'Pinterest-Generated-By': ''
+      'Pinterest-Generated-By': '',
+      'Content-Length': str(sample_content_length)
     }
 
 
@@ -31,18 +33,22 @@ def test_get_date_locale():
 def test_get_rate_limit():
     obj = Headers(sample_header)
 
-    assert obj.rate_limit == 200
+    assert obj.rate_limit == sample_rate_limit
 
 def test_get_rate_max():
     obj = Headers(sample_header)
 
-    assert obj.rate_remaining == 150
+    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
 
 if __name__ == "__main__":
     pytest.main([__file__, "-v", "-s"])
\ No newline at end of file