summaryrefslogtreecommitdiff
path: root/urlgrabber/progress.py
diff options
context:
space:
mode:
Diffstat (limited to 'urlgrabber/progress.py')
-rwxr-xr-x[-rw-r--r--]urlgrabber/progress.py341
1 files changed, 250 insertions, 91 deletions
diff --git a/urlgrabber/progress.py b/urlgrabber/progress.py
index dd07c6a..5b4c450 100644..100755
--- a/urlgrabber/progress.py
+++ b/urlgrabber/progress.py
@@ -9,23 +9,31 @@
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
-# License along with this library; if not, write to the
-# Free Software Foundation, Inc.,
-# 59 Temple Place, Suite 330,
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc.,
+# 59 Temple Place, Suite 330,
# Boston, MA 02111-1307 USA
# This file is part of urlgrabber, a high-level cross-protocol url-grabber
# Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
+from __future__ import print_function
import sys
import time
import math
-import thread
import fcntl
import struct
import termios
+if sys.version_info >= (3,):
+ # We use a version check because python2 also has _thread
+ import _thread as thread
+else:
+ import thread
+
+from six import integer_types, string_types
+
# Code from http://mail.python.org/pipermail/python-list/2000-May/033365.html
def terminal_width(fd=1):
""" Get the real terminal width """
@@ -107,7 +115,7 @@ class BaseMeter:
self.last_amount_read = 0
self.last_update_time = None
self.re = RateEstimator()
-
+
def start(self, filename=None, url=None, basename=None,
size=None, now=None, text=None):
self.filename = filename
@@ -125,7 +133,7 @@ class BaseMeter:
self.last_amount_read = 0
self.last_update_time = now
self._do_start(now)
-
+
def _do_start(self, now=None):
pass
@@ -133,8 +141,8 @@ class BaseMeter:
# for a real gui, you probably want to override and put a call
# to your mainloop iteration function here
if now is None: now = time.time()
- if (now >= self.last_update_time + self.update_period) or \
- not self.last_update_time:
+ if (not self.last_update_time or
+ (now >= self.last_update_time + self.update_period)):
self.re.update(amount_read, now)
self.last_amount_read = amount_read
self.last_update_time = now
@@ -152,7 +160,7 @@ class BaseMeter:
def _do_end(self, amount_read, now=None):
pass
-
+
# This is kind of a hack, but progress is gotten from grabber which doesn't
# know about the total size to download. So we do this so we can get the data
# out of band here. This will be "fixed" one way or anther soon.
@@ -167,7 +175,7 @@ def text_meter_total_size(size, downloaded=0):
#
# update: No size (minimal: 17 chars)
# -----------------------------------
-# <text> <rate> | <current size> <elapsed time>
+# <text> <rate> | <current size> <elapsed time>
# 8-48 1 8 3 6 1 9 5
#
# Order: 1. <text>+<current size> (17)
@@ -202,7 +210,7 @@ def text_meter_total_size(size, downloaded=0):
#
# end
# ---
-# <text> | <current size> <elapsed time>
+# <text> | <current size> <elapsed time>
# 8-56 3 6 1 9 5
#
# Order: 1. <text> ( 8)
@@ -211,6 +219,21 @@ def text_meter_total_size(size, downloaded=0):
# 4. + ( 5, total: 32)
#
+def _term_add_bar(tl, bar_max_length, pc):
+ blen = bar_max_length
+ bar = '='*int(blen * pc)
+ if (blen * pc) - int(blen * pc) >= 0.5:
+ bar += '-'
+ return tl.add(' [%-*.*s]' % (blen, blen, bar))
+
+def _term_add_end(tl, osize, size):
+ if osize: # osize should be None or >0, but that's been broken.
+ if size > osize: # Is ??? better? Really need something to say < vs >.
+ return tl.add(' !!! '), True
+ elif size != osize:
+ return tl.add(' ... '), True
+ return tl.add(' ' * 5), False
+
class TextMeter(BaseMeter):
def __init__(self, fo=sys.stderr):
BaseMeter.__init__(self)
@@ -218,7 +241,6 @@ class TextMeter(BaseMeter):
def _do_update(self, amount_read, now=None):
etime = self.re.elapsed_time()
- fetime = format_time(etime)
fread = format_number(amount_read)
#self.size = None
if self.text is not None:
@@ -234,19 +256,23 @@ class TextMeter(BaseMeter):
# Include text + ui_rate in minimal
tl = TerminalLine(8, 8+1+8)
+ if tl._llen > 80:
+ use_hours = True # For big screens, make it more readable.
+ else:
+ use_hours = False
ui_size = tl.add(' | %5sB' % fread)
if self.size is None:
- ui_time = tl.add(' %9s' % fetime)
+ ui_time = tl.add(' %s' % format_time(etime, use_hours))
ui_end = tl.add(' ' * 5)
ui_rate = tl.add(' %5sB/s' % ave_dl)
out = '%-*.*s%s%s%s%s\r' % (tl.rest(), tl.rest(), text,
ui_rate, ui_size, ui_time, ui_end)
else:
rtime = self.re.remaining_time()
- frtime = format_time(rtime)
+ frtime = format_time(rtime, use_hours)
frac = self.re.fraction_read()
- ui_time = tl.add(' %9s' % frtime)
+ ui_time = tl.add(' %s' % frtime)
ui_end = tl.add(' ETA ')
if sofar_size is None:
@@ -259,13 +285,10 @@ class TextMeter(BaseMeter):
ui_rate = tl.add(' %5sB/s' % ave_dl)
# Make text grow a bit before we start growing the bar too
blen = 4 + tl.rest_split(8 + 8 + 4)
- bar = '='*int(blen * frac)
- if (blen * frac) - int(blen * frac) >= 0.5:
- bar += '-'
- ui_bar = tl.add(' [%-*.*s]' % (blen, blen, bar))
- out = '%-*.*s%s%s%s%s%s%s%s\r' % (tl.rest(), tl.rest(), text,
- ui_sofar_pc, ui_pc, ui_bar,
- ui_rate, ui_size, ui_time, ui_end)
+ ui_bar = _term_add_bar(tl, blen, frac)
+ out = '\r%-*.*s%s%s%s%s%s%s%s\r' % (tl.rest(), tl.rest(), text,
+ ui_sofar_pc, ui_pc, ui_bar,
+ ui_rate,ui_size,ui_time, ui_end)
self.fo.write(out)
self.fo.flush()
@@ -274,7 +297,6 @@ class TextMeter(BaseMeter):
global _text_meter_total_size
global _text_meter_sofar_size
- total_time = format_time(self.re.elapsed_time())
total_size = format_number(amount_read)
if self.text is not None:
text = self.text
@@ -282,14 +304,13 @@ class TextMeter(BaseMeter):
text = self.basename
tl = TerminalLine(8)
- ui_size = tl.add(' | %5sB' % total_size)
- ui_time = tl.add(' %9s' % total_time)
- not_done = self.size is not None and amount_read != self.size
- if not_done:
- ui_end = tl.add(' ... ')
+ if tl._llen > 80:
+ use_hours = True # For big screens, make it more readable.
else:
- ui_end = tl.add(' ' * 5)
-
+ use_hours = False
+ ui_size = tl.add(' | %5sB' % total_size)
+ ui_time = tl.add(' %s' % format_time(self.re.elapsed_time(), use_hours))
+ ui_end, not_done = _term_add_end(tl, self.size, amount_read)
out = '\r%-*.*s%s%s%s\n' % (tl.rest(), tl.rest(), text,
ui_size, ui_time, ui_end)
self.fo.write(out)
@@ -331,14 +352,23 @@ class MultiFileHelper(BaseMeter):
def message(self, message):
self.master.message_meter(self, message)
+class _FakeLock:
+ def acquire(self):
+ pass
+ def release(self):
+ pass
+
class MultiFileMeter:
helperclass = MultiFileHelper
- def __init__(self):
+ def __init__(self, threaded=True):
self.meters = []
self.in_progress_meters = []
- self._lock = thread.allocate_lock()
+ if threaded:
+ self._lock = thread.allocate_lock()
+ else:
+ self._lock = _FakeLock()
self.update_period = 0.3 # seconds
-
+
self.numfiles = None
self.finished_files = 0
self.failed_files = 0
@@ -369,8 +399,9 @@ class MultiFileMeter:
def end(self, now=None):
if now is None: now = time.time()
+ self.re.update(self._amount_read(), now)
self._do_end(now)
-
+
def _do_end(self, now):
pass
@@ -383,10 +414,10 @@ class MultiFileMeter:
newmeter = self.helperclass(self)
self.meters.append(newmeter)
return newmeter
-
+
def removeMeter(self, meter):
self.meters.remove(meter)
-
+
###########################################################
# child functions - these should only be called by helpers
def start_meter(self, meter, now):
@@ -400,15 +431,15 @@ class MultiFileMeter:
finally:
self._lock.release()
self._do_start_meter(meter, now)
-
+
def _do_start_meter(self, meter, now):
pass
-
+
def update_meter(self, meter, now):
if not meter in self.meters:
raise ValueError('attempt to use orphaned meter')
- if (now >= self.last_update_time + self.update_period) or \
- not self.last_update_time:
+ if (not self.last_update_time or
+ (now >= self.last_update_time + self.update_period)):
self.re.update(self._amount_read(), now)
self.last_update_time = now
self._do_update_meter(meter, now)
@@ -466,34 +497,83 @@ class MultiFileMeter:
class TextMultiFileMeter(MultiFileMeter):
- def __init__(self, fo=sys.stderr):
+ def __init__(self, fo=sys.stderr, threaded=True):
self.fo = fo
- MultiFileMeter.__init__(self)
+ MultiFileMeter.__init__(self, threaded)
+ self.index_time = self.index = 0
# files: ###/### ###% data: ######/###### ###% time: ##:##:##/##:##:##
+# New output, like TextMeter output...
+# update: No size (minimal: 17 chars)
+# -----------------------------------
+# (<#file>/<#tot files>): <text> <rate> | <current size> <elapsed>
+# 8-48 1 8 3 6 1 7-9 5
+#
+# update: Size, All files
+# -----------------------
+# (<#file>/<#tot files>): <text> <pc> <bar> <rate> | <size> <eta time> ETA
+# 8-22 1 3-4 1 6-12 1 8 3 6 1 7-9 1 3 1
+# end
+# ---
+# <text> | <file size> <file elapsed time>
+# 8-56 3 6 1 9 5
def _do_update_meter(self, meter, now):
self._lock.acquire()
try:
- format = "files: %3i/%-3i %3i%% data: %6.6s/%-6.6s %3i%% " \
- "time: %8.8s/%8.8s"
df = self.finished_files
tf = self.numfiles or 1
- pf = 100 * float(df)/tf + 0.49
+ # Don't use "percent of files complete" ...
+ # pf = 100 * float(df)/tf + 0.49
dd = self.re.last_amount_read
- td = self.total_size
+ td = self.re.total
pd = 100 * (self.re.fraction_read() or 0) + 0.49
dt = self.re.elapsed_time()
rt = self.re.remaining_time()
- if rt is None: tt = None
- else: tt = dt + rt
-
- fdd = format_number(dd) + 'B'
- ftd = format_number(td) + 'B'
- fdt = format_time(dt, 1)
- ftt = format_time(tt, 1)
-
- out = '%-79.79s' % (format % (df, tf, pf, fdd, ftd, pd, fdt, ftt))
- self.fo.write('\r' + out)
+
+ frac = self.re.fraction_read() or 0
+ pf = 100 * frac
+ ave_dl = format_number(self.re.average_rate())
+
+ # cycle through active meters
+ if now > self.index_time:
+ self.index_time = now + 1.0
+ self.index += 1
+ if self.index >= len(self.meters):
+ self.index = 0
+ meter = self.meters[self.index]
+ text = meter.text or meter.basename
+ if tf > 1:
+ text = '(%u/%u): %s' % (df+1+self.index, tf, text)
+
+ # Include text + ui_rate in minimal
+ tl = TerminalLine(8, 8+1+8)
+ if tl._llen > 80:
+ use_hours = True # For big screens, make it more readable.
+ else:
+ use_hours = False
+ ui_size = tl.add(' | %5sB' % format_number(dd))
+ if not self.re.total:
+ ui_time = tl.add(' %s' % format_time(dt, use_hours))
+ ui_end = tl.add(' ' * 5)
+ ui_rate = tl.add(' %5sB/s' % ave_dl)
+ out = '\r%-*.*s%s%s%s%s\r' % (tl.rest(), tl.rest(), text,
+ ui_rate, ui_size, ui_time, ui_end)
+ else:
+ ui_time = tl.add(' %s' % format_time(rt, use_hours))
+ ui_end = tl.add(' ETA ')
+
+ ui_sofar_pc = tl.add(' %i%%' % pf,
+ full_len=len(" (100%)"))
+ ui_rate = tl.add(' %5sB/s' % ave_dl)
+
+ # Make text grow a bit before we start growing the bar too
+ blen = 4 + tl.rest_split(8 + 8 + 4)
+ ui_bar = _term_add_bar(tl, blen, frac)
+ out = '\r%-*.*s%s%s%s%s%s%s\r' % (tl.rest(), tl.rest(), text,
+ ui_sofar_pc, ui_bar,
+ ui_rate, ui_size, ui_time,
+ ui_end)
+ self.fo.write(out)
self.fo.flush()
finally:
self._lock.release()
@@ -502,25 +582,39 @@ class TextMultiFileMeter(MultiFileMeter):
self._lock.acquire()
try:
format = "%-30.30s %6.6s %8.8s %9.9s"
- fn = meter.basename
+ fn = meter.text or meter.basename
size = meter.last_amount_read
fsize = format_number(size) + 'B'
et = meter.re.elapsed_time()
- fet = format_time(et, 1)
- frate = format_number(size / et) + 'B/s'
-
- out = '%-79.79s' % (format % (fn, fsize, fet, frate))
- self.fo.write('\r' + out + '\n')
+ frate = format_number(et and size / et) + 'B/s'
+ df = self.finished_files
+ tf = self.numfiles or 1
+
+ total_size = format_number(size)
+ text = meter.text or meter.basename
+ if tf > 1:
+ text = '(%u/%u): %s' % (df, tf, text)
+
+ tl = TerminalLine(8)
+ if tl._llen > 80:
+ use_hours = True # For big screens, make it more readable.
+ else:
+ use_hours = False
+ ui_size = tl.add(' | %5sB' % total_size)
+ ui_time = tl.add(' %s' % format_time(et, use_hours))
+ ui_end, not_done = _term_add_end(tl, meter.size, size)
+ out = '\r%-*.*s%s%s%s\n' % (tl.rest(), tl.rest(), text,
+ ui_size, ui_time, ui_end)
+ self.fo.write(out)
finally:
self._lock.release()
- self._do_update_meter(meter, now)
def _do_failure_meter(self, meter, message, now):
self._lock.acquire()
try:
format = "%-30.30s %6.6s %s"
- fn = meter.basename
- if type(message) in (type(''), type(u'')):
+ fn = meter.text or meter.basename
+ if isinstance(message, string_types):
message = message.splitlines()
if not message: message = ['']
out = '%-79s' % (format % (fn, 'FAILED', message[0] or ''))
@@ -537,15 +631,6 @@ class TextMultiFileMeter(MultiFileMeter):
finally:
self._lock.release()
- def _do_end(self, now):
- self._do_update_meter(None, now)
- self._lock.acquire()
- try:
- self.fo.write('\n')
- self.fo.flush()
- finally:
- self._lock.release()
-
######################################################################
# support classes and functions
@@ -560,13 +645,17 @@ class RateEstimator:
self.last_update_time = now
self.last_amount_read = 0
self.ave_rate = None
-
+
def update(self, amount_read, now=None):
if now is None: now = time.time()
- if amount_read == 0:
+ # libcurl calls the progress callback when fetching headers
+ # too, thus amount_read = 0 .. hdr_size .. 0 .. content_size.
+ # Ocassionally we miss the 2nd zero and report avg speed < 0.
+ # Handle read_diff < 0 here. BZ 1001767.
+ if amount_read == 0 or amount_read < self.last_amount_read:
# if we just started this file, all bets are off
self.last_update_time = now
- self.last_amount_read = 0
+ self.last_amount_read = amount_read
self.ave_rate = None
return
@@ -576,11 +665,11 @@ class RateEstimator:
# First update, on reget is the file size
if self.last_amount_read:
self.last_update_time = now
- self.ave_rate = self._temporal_rolling_ave(\
+ self.ave_rate = self._temporal_rolling_ave(
time_diff, read_diff, self.ave_rate, self.timescale)
self.last_amount_read = amount_read
#print 'results', time_diff, read_diff, self.ave_rate
-
+
#####################################################################
# result methods
def average_rate(self):
@@ -616,14 +705,14 @@ class RateEstimator:
epsilon = time_diff / timescale
if epsilon > 1: epsilon = 1.0
return self._rolling_ave(time_diff, read_diff, last_ave, epsilon)
-
+
def _rolling_ave(self, time_diff, read_diff, last_ave, epsilon):
"""perform a "rolling average" iteration
a rolling average "folds" new data into an existing average with
some weight, epsilon. epsilon must be between 0.0 and 1.0 (inclusive)
a value of 0.0 means only the old value (initial value) counts,
and a value of 1.0 means only the newest value is considered."""
-
+
try:
recent_rate = read_diff / time_diff
except ZeroDivisionError:
@@ -652,23 +741,25 @@ class RateEstimator:
rt = int(rt)
if shift <= 0: return rt
return float(int(rt) >> shift << shift)
-
+
def format_time(seconds, use_hours=0):
if seconds is None or seconds < 0:
if use_hours: return '--:--:--'
else: return '--:--'
+ elif seconds == float('inf'):
+ return 'Infinite'
else:
seconds = int(seconds)
- minutes = seconds / 60
+ minutes = seconds // 60
seconds = seconds % 60
if use_hours:
- hours = minutes / 60
+ hours = minutes // 60
minutes = minutes % 60
return '%02i:%02i:%02i' % (hours, minutes, seconds)
else:
return '%02i:%02i' % (minutes, seconds)
-
+
def format_number(number, SI=0, space=' '):
"""Turn numbers into human-readable metric-like numbers"""
symbols = ['', # (none)
@@ -680,14 +771,14 @@ def format_number(number, SI=0, space=' '):
'E', # exa
'Z', # zetta
'Y'] # yotta
-
+
if SI: step = 1000.0
else: step = 1024.0
thresh = 999
depth = 0
max_depth = len(symbols) - 1
-
+
# we want numbers between 0 and thresh, but don't exceed the length
# of our list. In that event, the formatting will be screwed up,
# but it'll still show the right number.
@@ -695,7 +786,7 @@ def format_number(number, SI=0, space=' '):
depth = depth + 1
number = number / step
- if type(number) == type(1) or type(number) == type(1L):
+ if isinstance(number, integer_types):
# it's an int or a long, which means it didn't get divided,
# which means it's already short enough
format = '%i%s%s'
@@ -705,7 +796,7 @@ def format_number(number, SI=0, space=' '):
format = '%.1f%s%s'
else:
format = '%.0f%s%s'
-
+
return(format % (float(number or 0), space, symbols[depth]))
def _tst(fn, cur, tot, beg, size, *args):
@@ -722,9 +813,77 @@ def _tst(fn, cur, tot, beg, size, *args):
time.sleep(delay)
tm.end(size)
+def _mtst(datas, *args):
+ print('-' * 79)
+ tm = TextMultiFileMeter(threaded=False)
+
+ dl_sizes = {}
+
+ num = 0
+ total_size = 0
+ dl_total_size = 0
+ for data in datas:
+ dl_size = None
+ if len(data) == 2:
+ fn, size = data
+ dl_size = size
+ if len(data) == 3:
+ fn, size, dl_size = data
+ nm = tm.newMeter()
+ nm.start(fn, "http://www.example.com/path/to/fn/" + fn, fn, size,
+ text=fn)
+ num += 1
+ assert dl_size is not None
+ dl_total_size += dl_size
+ dl_sizes[nm] = dl_size
+ if size is None or total_size is None:
+ total_size = None
+ else:
+ total_size += size
+ tm.start(num, total_size)
+
+ num = 0
+ off = 0
+ for (inc, delay) in args:
+ off += 1
+ while num < ((dl_total_size * off) / len(args)):
+ num += inc
+ for nm in tm.meters[:]:
+ if dl_sizes[nm] <= num:
+ nm.end(dl_sizes[nm])
+ tm.removeMeter(nm)
+ else:
+ nm.update(num)
+ time.sleep(delay)
+ assert not tm.meters
+
if __name__ == "__main__":
- # (1/2): subversion-1.4.4-7.x86_64.rpm 2.4 MB / 85 kB/s 00:28
- # (2/2): mercurial-0.9.5-6.fc8.x86_64.rpm 924 kB / 106 kB/s 00:08
+ # (1/2): subversion-1.4.4-7.x86_64.rpm 2.4 MB / 85 kB/s 00:28
+ # (2/2): mercurial-0.9.5-6.fc8.x86_64.rpm 924 kB / 106 kB/s 00:08
+ if len(sys.argv) >= 2 and sys.argv[1] == 'multi':
+ _mtst((("sm-1.0.0-1.fc8.i386.rpm", 1000),
+ ("s-1.0.1-1.fc8.i386.rpm", 5000),
+ ("m-1.0.1-2.fc8.i386.rpm", 10000)),
+ (100, 0.33), (500, 0.25), (1000, 0.1))
+
+ _mtst((("sm-1.0.0-1.fc8.i386.rpm", 1000),
+ ("s-1.0.1-1.fc8.i386.rpm", 5000),
+ ("m-1.0.1-2.fc8.i386.rpm", None, 10000)),
+ (100, 0.33), (500, 0.25), (1000, 0.1))
+
+ _mtst((("sm-1.0.0-1.fc8.i386.rpm", 1000),
+ ("s-1.0.1-1.fc8.i386.rpm", 2500000),
+ ("m-1.0.1-2.fc8.i386.rpm", 10000)),
+ (10, 0.2), (50, 0.1), (1000, 0.1))
+
+ _mtst((("sm-1.0.0-1.fc8.i386.rpm", 1000),
+ ("s-1.0.1-1.fc8.i386.rpm", None, 2500000),
+ ("m-1.0.1-2.fc8.i386.rpm", None, 10000)),
+ (10, 0.2), (50, 0.1), (1000, 0.1))
+ # (10, 0.2), (100, 0.1), (100, 0.1), (100, 0.25))
+ # (10, 0.2), (100, 0.1), (100, 0.1), (100, 0.25))
+ sys.exit(0)
+
if len(sys.argv) >= 2 and sys.argv[1] == 'total':
text_meter_total_size(1000 + 10000 + 10000 + 1000000 + 1000000 +
1000000 + 10000 + 10000 + 10000 + 1000000)