├── requirements.txt ├── MANIFEST.in ├── .gitignore ├── .travis.yml ├── CHANGELOG.rst ├── setup.py ├── LICENSE ├── README.md ├── tests.py └── homura.py /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi 2 | humanize 3 | six 4 | pycurl 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.rst 2 | include LICENSE 3 | include MANIFEST.in 4 | include README.md 5 | include requirements.txt 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | .DS_Store 3 | *.swp 4 | *.bak 5 | build 6 | dist 7 | *.egg-info 8 | .tox 9 | run.py 10 | .credentials 11 | tags 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | - "3.3" 6 | - "3.4" 7 | 8 | branches: 9 | only: 10 | - master 11 | 12 | install: 13 | - pip install nose -r requirements.txt 14 | 15 | script: nosetests 16 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.1.5 (2017-01-21) 5 | ------------------ 6 | 7 | - Add HTTP basic auth support. 8 | 9 | 0.1.4 (2017-01-20) 10 | ------------------ 11 | 12 | - Add argument ``user_agent``. 13 | 14 | 0.1.3 (2015-09-26) 15 | ------------------ 16 | 17 | - Fixed `#5 `_ and merged `#6 `_. 18 | 19 | 0.1.2 (2015-09-16) 20 | ------------------ 21 | 22 | - Added argument ``pass_through_opts``. 23 | 24 | 0.1.1 (2015-04-14) 25 | ------------------ 26 | 27 | - Fixed unicode issues complained by ``urlparse`` and ``os.path.join`` in Python 2.x and 3.3. 28 | 29 | 30 | 0.1.0 (2015-01-16) 31 | ------------------ 32 | 33 | - Initial release. 34 | 35 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from codecs import open as codecs_open 3 | 4 | with codecs_open('README.md', encoding='utf-8') as f: 5 | lng_desc = f.read() 6 | 7 | setup( 8 | name='homura', 9 | version='0.1.5', 10 | description="Python downloader with progress", 11 | long_description=lng_desc, 12 | keywords='download progress', 13 | author='Shichao An', 14 | author_email='shichao.an@nyu.edu', 15 | url='https://github.com/shichao-an/homura', 16 | license='BSD', 17 | install_requires=open('requirements.txt').read().splitlines(), 18 | py_modules=['homura'], 19 | include_package_data=True, 20 | zip_safe=False, 21 | classifiers=[ 22 | "License :: OSI Approved :: BSD License", 23 | "Programming Language :: Python", 24 | "Programming Language :: Python :: 2", 25 | "Programming Language :: Python :: 3", 26 | ], 27 | ) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Shichao An 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Homura 2 | 3 | [![Build Status][travis-image]][travis-link] 4 | [![PyPI Version][pypi-image]][pypi-link] 5 | 6 | Homura (ほむら) is a Python downloader with progress, which can be used to download large files. 7 | 8 | It is named after [Homura Akemi](https://en.wikipedia.org/wiki/Homura_Akemi). 9 | 10 | ### Features 11 | 12 | * PycURL based 13 | * Resume downloads (if server supports [byte ranges](http://en.wikipedia.org/wiki/Byte_serving) on the resource) 14 | * Support for `requests.Session` 15 | 16 | ### Installation 17 | 18 | Homura depends on [PycURL](http://pycurl.sourceforge.net/). Install dependencies before installing the python package: 19 | 20 | #### Ubuntu 21 | 22 | ```bash 23 | sudo apt-get install build-essential libcurl4-openssl-dev python-dev 24 | ``` 25 | 26 | #### Fedora 27 | 28 | Yum: 29 | 30 | ```bash 31 | sudo yum groupinstall "Development Tools" 32 | sudo yum install libcurl libcurl-devel python-devel 33 | ``` 34 | 35 | DNF: 36 | 37 | ```bash 38 | sudo dnf groupinstall "Development Tools" 39 | sudo dnf install libcurl libcurl-devel python-devel 40 | ``` 41 | 42 | #### Install Homura 43 | 44 | ```bash 45 | pip install homura 46 | ``` 47 | 48 | ### Usage 49 | 50 | The simplest usage is to import the utility function `download`: 51 | 52 | ```python 53 | from homura import download 54 | download('http://download.thinkbroadband.com/200MB.zip') 55 | 3% 6.2 MiB 739.5 KiB/s 0:04:28 ETA 56 | ``` 57 | 58 | To specify path for downloaded file: 59 | 60 | ```python 61 | download(url='http://download.thinkbroadband.com/200MB.zip', 62 | path='/path/to/big.zip') 63 | ``` 64 | 65 | You can specify extra headers as a dictionary: 66 | 67 | ```python 68 | download(url='http://example.com', headers={'API-Key': '123456'}) 69 | ``` 70 | 71 | You can work with `Session` objects of the [requests](http://docs.python-requests.org/en/latest/) library: 72 | 73 | ```python 74 | import requests 75 | s = requests.Session() 76 | # Do some work with `s` and send requests 77 | download(url='http://example.com', session=s) 78 | ``` 79 | 80 | Pass options to `setopt` of the `pycurl.Curl` object via the `pass_through_opts` argument: 81 | 82 | ```python 83 | import pycurl 84 | download(url=url, pass_through_opts={pycurl.FOLLOWLOCATION: True}) 85 | ``` 86 | 87 | [travis-image]: https://api.travis-ci.org/shichao-an/homura.png?branch=master 88 | [travis-link]: https://travis-ci.org/shichao-an/homura 89 | [pypi-image]: https://img.shields.io/pypi/v/homura.png 90 | [pypi-link]: https://pypi.python.org/pypi/homura/ 91 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import pycurl 4 | import shutil 5 | from unittest import TestCase 6 | 7 | from homura import download, get_resource_name, utf8_encode 8 | 9 | PROJECT_PATH = os.path.abspath(os.path.dirname(__file__)) 10 | TEST_DATA_DIR = os.path.join(PROJECT_PATH, 'test_data') 11 | TEST_DATA_SUBDIR = os.path.join(TEST_DATA_DIR, 'sub') 12 | TEST_DATA_ASCII = TEST_DATA_SUBDIR 13 | TEST_DATA_UNICODE = os.path.join(TEST_DATA_DIR, u'下载') 14 | TEST_DATA_UTF8 = utf8_encode(os.path.join(TEST_DATA_DIR, u'离线')) 15 | SUBDIR_RELPATH = os.path.basename(TEST_DATA_SUBDIR) 16 | FILE_SMALL = 'http://download.thinkbroadband.com/MD5SUMS' 17 | FILE_1MB = 'http://download.thinkbroadband.com/1MB.zip' 18 | FILE_5MB = 'http://download.thinkbroadband.com/5MB.zip' 19 | FILE_301_SMALL = 'https://dev.moleculea.com/homura/301/SMD5SUMS' 20 | FILE_301_1MB = 'https://dev.moleculea.com/homura/301/S1MB.zip' 21 | FILE_301_5MB = 'https://dev.moleculea.com/homura/301/S5MB.zip' 22 | FILE_UNICODE = u'https://dev.moleculea.com/离线下载.txt' 23 | FILE_UTF8 = utf8_encode(u'http://dev.moleculea.com/离线下载.txt') 24 | 25 | 26 | def cleanup_data(): 27 | os.chdir(PROJECT_PATH) 28 | if os.path.exists(TEST_DATA_DIR): 29 | shutil.rmtree(TEST_DATA_DIR) 30 | 31 | 32 | class TestDownload(TestCase): 33 | """Test homura.download""" 34 | def setUp(self): 35 | cleanup_data() 36 | os.mkdir(TEST_DATA_DIR) 37 | os.mkdir(TEST_DATA_SUBDIR) 38 | os.mkdir(TEST_DATA_UNICODE) 39 | os.mkdir(TEST_DATA_UTF8) 40 | os.chdir(TEST_DATA_DIR) 41 | 42 | def test_simple(self): 43 | download(FILE_1MB) 44 | f = os.path.join(TEST_DATA_DIR, get_resource_name(FILE_1MB)) 45 | assert os.path.exists(f) 46 | os.remove(f) 47 | 48 | def test_path(self): 49 | url = FILE_SMALL 50 | 51 | # path='' 52 | download(url=url, path='') 53 | f = os.path.join(TEST_DATA_DIR, get_resource_name(url)) 54 | assert os.path.exists(f) 55 | os.remove(f) 56 | 57 | # path='.' 58 | download(url=url, path='.') 59 | f = os.path.join(TEST_DATA_DIR, get_resource_name(url)) 60 | assert os.path.exists(f) 61 | os.remove(f) 62 | 63 | # path=TEST_DATA_SUBDIR 64 | download(url=url, path=TEST_DATA_SUBDIR) 65 | f = os.path.join(TEST_DATA_SUBDIR, get_resource_name(url)) 66 | assert os.path.exists(f) 67 | os.remove(f) 68 | 69 | # path='foobar' 70 | download(url=url, path='foobar') 71 | f = os.path.join(TEST_DATA_DIR, 'foobar') 72 | assert os.path.exists(f) 73 | os.remove(f) 74 | 75 | # path='foo/bar' 76 | with self.assertRaises(IOError): 77 | download(url=url, path='foo/bar') 78 | f = os.path.join(TEST_DATA_DIR, 'foo', 'bar') 79 | assert not os.path.exists(f) 80 | 81 | def test_redirect(self): 82 | url = FILE_301_SMALL 83 | eurl = FILE_SMALL 84 | 85 | # No path 86 | download(url=url) 87 | f = os.path.join(TEST_DATA_DIR, get_resource_name(url)) 88 | ef = os.path.join(TEST_DATA_DIR, get_resource_name(eurl)) 89 | assert not os.path.exists(f) 90 | assert os.path.exists(ef) 91 | os.remove(ef) 92 | 93 | # path='foobar' 94 | download(url=url, path='foobar') 95 | f = os.path.join(TEST_DATA_DIR, 'foobar') 96 | assert os.path.exists(f) 97 | os.remove(f) 98 | 99 | def test_unicode(self): 100 | url = FILE_UNICODE 101 | path_ascii = TEST_DATA_ASCII 102 | path_unicode = TEST_DATA_UNICODE 103 | path_utf8 = TEST_DATA_UTF8 104 | 105 | # No path 106 | download(url=url) 107 | f = os.path.join(TEST_DATA_DIR, get_resource_name(url)) 108 | assert os.path.exists(f) 109 | os.remove(f) 110 | 111 | # ASCII path 112 | download(url=url, path=path_ascii) 113 | f = os.path.join(TEST_DATA_DIR, path_ascii, get_resource_name(url)) 114 | assert os.path.exists(f) 115 | os.remove(f) 116 | 117 | # Unicode path 118 | download(url=url, path=path_unicode) 119 | f = os.path.join(TEST_DATA_DIR, path_unicode, get_resource_name(url)) 120 | assert os.path.exists(f) 121 | os.remove(f) 122 | 123 | # UTF-8 path 124 | download(url=url, path=path_utf8) 125 | f = os.path.join(utf8_encode(TEST_DATA_DIR), path_utf8, 126 | utf8_encode(get_resource_name(url))) 127 | assert os.path.exists(f) 128 | os.remove(f) 129 | 130 | def test_utf8(self): 131 | url = FILE_UTF8 132 | path_ascii = TEST_DATA_ASCII 133 | path_unicode = TEST_DATA_UNICODE 134 | path_utf8 = TEST_DATA_UTF8 135 | 136 | # No path 137 | download(url=url) 138 | f = os.path.join(TEST_DATA_DIR, get_resource_name(url)) 139 | assert os.path.exists(f) 140 | os.remove(f) 141 | 142 | # ASCII path 143 | download(url=url, path=path_ascii) 144 | f = os.path.join(TEST_DATA_DIR, path_ascii, get_resource_name(url)) 145 | assert os.path.exists(f) 146 | os.remove(f) 147 | 148 | # Unicode path 149 | download(url=url, path=path_unicode) 150 | f = os.path.join(TEST_DATA_DIR, path_unicode, get_resource_name(url)) 151 | assert os.path.exists(f) 152 | os.remove(f) 153 | 154 | # UTF-8 path 155 | download(url=url, path=path_utf8) 156 | f = os.path.join(utf8_encode(TEST_DATA_DIR), path_utf8, 157 | utf8_encode(get_resource_name(url))) 158 | assert os.path.exists(f) 159 | os.remove(f) 160 | 161 | def test_pass_through_opts(self): 162 | url = FILE_5MB 163 | opts_url = FILE_1MB 164 | 165 | download(url=url, pass_through_opts={pycurl.URL: opts_url}) 166 | f = os.path.join(TEST_DATA_DIR, get_resource_name(url)) 167 | opts_f = os.path.join(TEST_DATA_DIR, get_resource_name(opts_url)) 168 | assert os.path.exists(opts_f) 169 | assert not os.path.exists(f) 170 | os.remove(opts_f) 171 | 172 | def test_auth(self): 173 | url = "http://httpbin.org/basic-auth/aaa/bbb" 174 | auth = ("aaa", "bbb") 175 | 176 | download(url=url, auth=auth) 177 | f = os.path.join(TEST_DATA_DIR, get_resource_name(url)) 178 | with open(f) as handle: 179 | txt = handle.read() 180 | assert '"authenticated": true' in txt 181 | assert '"user": "aaa"' in txt 182 | assert os.path.exists(f) 183 | os.remove(f) 184 | 185 | def tearDown(self): 186 | cleanup_data() 187 | -------------------------------------------------------------------------------- /homura.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function, absolute_import 3 | import datetime 4 | import os 5 | import six 6 | import sys 7 | import time 8 | import pycurl 9 | import shutil 10 | from six.moves.urllib.parse import urlparse, unquote as _unquote 11 | from humanize import naturalsize 12 | 13 | try: 14 | import certifi 15 | except ImportError: 16 | certifi = None 17 | 18 | PY3 = sys.version_info[0] == 3 19 | STREAM = sys.stderr 20 | DEFAULT_RESOURCE = 'index.html' 21 | 22 | __version__ = '0.1.5' 23 | 24 | 25 | def eval_path(path): 26 | return os.path.abspath(os.path.expanduser(path)) 27 | 28 | 29 | def utf8_encode(s): 30 | res = s 31 | if isinstance(res, six.text_type): 32 | res = s.encode('utf-8') 33 | return res 34 | 35 | 36 | def utf8_decode(s): 37 | res = s 38 | if isinstance(res, six.binary_type): 39 | res = s.decode('utf-8') 40 | return res 41 | 42 | 43 | def unquote(s): 44 | res = s 45 | if not PY3: 46 | if isinstance(res, six.text_type): 47 | res = s.encode('utf-8') 48 | return _unquote(res) 49 | 50 | 51 | def dict_to_list(d): 52 | return ['%s: %s' % (k, v) for k, v in d.items()] 53 | 54 | 55 | def is_temp_path(path): 56 | if path is None: 57 | return True 58 | else: 59 | path = eval_path(path) 60 | if os.path.isdir(path): 61 | return True 62 | return False 63 | 64 | 65 | def get_resource_name(url): 66 | url = utf8_decode(url) # decode to unicode so PY3's urlparse won't break 67 | o = urlparse(url) 68 | resource = os.path.basename(o.path) 69 | if not resource: 70 | res = DEFAULT_RESOURCE 71 | else: 72 | res = resource 73 | return utf8_decode(unquote(res)) 74 | 75 | 76 | class Homura(object): 77 | progress_template = \ 78 | '%(percent)6d%% %(downloaded)12s %(speed)15s %(eta)18s ETA' + ' ' * 4 79 | eta_limit = 2592000 # 30 days 80 | 81 | def __init__(self, url, path=None, headers=None, session=None, 82 | show_progress=True, resume=True, auto_retry=True, 83 | max_rst_retries=5, pass_through_opts=None, cainfo=None, 84 | user_agent=None, auth=None): 85 | """ 86 | :param str url: URL of the file to be downloaded 87 | :param str path: local path for the downloaded file; if None, it will 88 | be the URL base name 89 | :param dict headers: extra headers 90 | :param session: session used to download (if you want to work with 91 | requests library) 92 | :type session: `class:requests.Session` 93 | :param bool show_progress: whether to show download progress 94 | :param bool resume: whether to resume download (by 95 | filename) 96 | :param bool auto_retry: whether to retry automatically upon closed 97 | transfer until the file's download is finished 98 | :param int max_rst_retries: number of retries upon connection reset by 99 | peer (effective only when `auto_retry` is True) 100 | :param dict pass_through_opts: a dictionary of options passed to cURL 101 | :param str cainfo: optional path to a PEM file containing the CA 102 | certificate 103 | :param str user_agent: set a custom user agent string 104 | :param tuple auth: a tuple of username and password for authentication 105 | """ 106 | self.url = url # url is in unicode 107 | self.path = self._get_path(path, url) 108 | self.headers = headers 109 | self.session = session 110 | self.show_progress = show_progress 111 | self.resume = resume 112 | self.auto_retry = auto_retry 113 | self.max_rst_retries = max_rst_retries 114 | self.cainfo = cainfo 115 | self.start_time = None 116 | self.content_length = None 117 | self.downloaded = 0 118 | self.auth = None 119 | if session: 120 | self.auth = session.auth 121 | if auth: 122 | self.auth = auth 123 | self._path = path # Save given path 124 | self._pycurl = pycurl.Curl() 125 | self._cookie_header = self._get_cookie_header() 126 | self._last_time = 0.0 127 | self._rst_retries = 0 128 | self._pass_through_opts = pass_through_opts 129 | self._user_agent = user_agent or 'homura/' + __version__ 130 | 131 | def _get_cookie_header(self): 132 | if self.session is not None: 133 | cookie = dict(self.session.cookies) 134 | res = [] 135 | for k, v in cookie.items(): 136 | s = '%s=%s' % (k, v) 137 | res.append(s) 138 | if not res: 139 | return None 140 | return '; '.join(res) 141 | 142 | def _get_path(self, path, url): 143 | if path is None: 144 | res = get_resource_name(url) 145 | else: 146 | # decode path to unicode so that os.path.join won't break 147 | res = eval_path(utf8_decode(path)) 148 | if os.path.isdir(res): 149 | resource = get_resource_name(url) 150 | res = os.path.join(res, resource) 151 | return res 152 | 153 | def _get_pycurl_headers(self): 154 | headers = self.headers or {} 155 | if self._cookie_header is not None: 156 | headers['Cookie'] = self._cookie_header 157 | return dict_to_list(headers) or None 158 | 159 | def _fill_in_cainfo(self): 160 | """Fill in the path of the PEM file containing the CA certificate. 161 | 162 | The priority is: 1. user provided path, 2. path to the cacert.pem 163 | bundle provided by certifi (if installed), 3. let pycurl use the 164 | system path where libcurl's cacert bundle is assumed to be stored, 165 | as established at libcurl build time. 166 | """ 167 | if self.cainfo: 168 | cainfo = self.cainfo 169 | else: 170 | try: 171 | cainfo = certifi.where() 172 | except AttributeError: 173 | cainfo = None 174 | if cainfo: 175 | self._pycurl.setopt(pycurl.CAINFO, cainfo) 176 | 177 | def curl(self): 178 | """Sending a single cURL request to download""" 179 | c = self._pycurl 180 | # Resume download 181 | if os.path.exists(self.path) and self.resume: 182 | mode = 'ab' 183 | self.downloaded = os.path.getsize(self.path) 184 | c.setopt(pycurl.RESUME_FROM, self.downloaded) 185 | else: 186 | mode = 'wb' 187 | with open(self.path, mode) as f: 188 | c.setopt(c.URL, utf8_encode(self.url)) 189 | if self.auth: 190 | c.setopt(c.USERPWD, '%s:%s' % self.auth) 191 | c.setopt(c.USERAGENT, self._user_agent) 192 | c.setopt(c.WRITEDATA, f) 193 | h = self._get_pycurl_headers() 194 | if h is not None: 195 | c.setopt(pycurl.HTTPHEADER, h) 196 | c.setopt(c.NOPROGRESS, 0) 197 | c.setopt(pycurl.FOLLOWLOCATION, 1) 198 | c.setopt(c.PROGRESSFUNCTION, self.progress) 199 | self._fill_in_cainfo() 200 | if self._pass_through_opts: 201 | for key, value in self._pass_through_opts.items(): 202 | c.setopt(key, value) 203 | c.perform() 204 | 205 | def start(self): 206 | """ 207 | Start downloading, handling auto retry, download resume and path 208 | moving 209 | """ 210 | if not self.auto_retry: 211 | self.curl() 212 | return 213 | while not self.is_finished: 214 | try: 215 | self.curl() 216 | except pycurl.error as e: 217 | # transfer closed with n bytes remaining to read 218 | if e.args[0] == pycurl.E_PARTIAL_FILE: 219 | pass 220 | # HTTP server doesn't seem to support byte ranges. 221 | # Cannot resume. 222 | elif e.args[0] == pycurl.E_HTTP_RANGE_ERROR: 223 | break 224 | # Recv failure: Connection reset by peer 225 | elif e.args[0] == pycurl.E_RECV_ERROR: 226 | if self._rst_retries < self.max_rst_retries: 227 | pass 228 | else: 229 | raise e 230 | self._rst_retries += 1 231 | else: 232 | raise e 233 | self._move_path() 234 | self._done() 235 | 236 | def progress(self, download_t, download_d, upload_t, upload_d): 237 | self.content_length = self.downloaded + int(download_t) 238 | if int(download_t) == 0: 239 | return 240 | if not self.show_progress: 241 | return 242 | if self.start_time is None: 243 | self.start_time = time.time() 244 | duration = time.time() - self.start_time + 1 245 | speed = download_d / duration 246 | speed_s = naturalsize(speed, binary=True) 247 | speed_s += '/s' 248 | if speed == 0.0: 249 | eta = self.eta_limit 250 | else: 251 | eta = int((download_t - download_d) / speed) 252 | if eta < self.eta_limit: 253 | eta_s = str(datetime.timedelta(seconds=eta)) 254 | else: 255 | eta_s = 'n/a' 256 | downloaded = self.downloaded + download_d 257 | downloaded_s = naturalsize(downloaded, binary=True) 258 | percent = int(downloaded / self.content_length * 100) 259 | params = { 260 | 'downloaded': downloaded_s, 261 | 'percent': percent, 262 | 'speed': speed_s, 263 | 'eta': eta_s, 264 | } 265 | if STREAM.isatty(): 266 | p = (self.progress_template + '\r') % params 267 | else: 268 | current_time = time.time() 269 | if self._last_time == 0.0: 270 | self._last_time = current_time 271 | else: 272 | interval = current_time - self._last_time 273 | if interval < 0.5: 274 | return 275 | self._last_time = current_time 276 | p = (self.progress_template + '\n') % params 277 | STREAM.write(p) 278 | STREAM.flush() 279 | 280 | @property 281 | def is_finished(self): 282 | if os.path.exists(self.path): 283 | return self.content_length == os.path.getsize(self.path) 284 | 285 | def _done(self): 286 | STREAM.write('\n') 287 | STREAM.flush() 288 | 289 | def _move_path(self): 290 | """ 291 | Move the downloaded file to the authentic path (identified by 292 | effective URL) 293 | """ 294 | if is_temp_path(self._path) and self._pycurl is not None: 295 | eurl = self._pycurl.getinfo(pycurl.EFFECTIVE_URL) 296 | er = get_resource_name(eurl) 297 | r = get_resource_name(self.url) 298 | if er != r and os.path.exists(self.path): 299 | new_path = self._get_path(self._path, eurl) 300 | shutil.move(self.path, new_path) 301 | self.path = new_path 302 | 303 | 304 | def download(url, path=None, headers=None, session=None, show_progress=True, 305 | resume=True, auto_retry=True, max_rst_retries=5, 306 | pass_through_opts=None, cainfo=None, user_agent=None, auth=None): 307 | """Main download function""" 308 | hm = Homura(url, path, headers, session, show_progress, resume, 309 | auto_retry, max_rst_retries, pass_through_opts, cainfo, 310 | user_agent, auth) 311 | hm.start() 312 | --------------------------------------------------------------------------------