1
# This library is free software; you can redistribute it and/or
2
# modify it under the terms of the GNU Lesser General Public
3
# License as published by the Free Software Foundation; either
4
# version 2.1 of the License, or (at your option) any later version.
6
# This library is distributed in the hope that it will be useful,
7
# but WITHOUT ANY WARRANTY; without even the implied warranty of
8
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9
# Lesser General Public License for more details.
11
# You should have received a copy of the GNU Lesser General Public
12
# License along with this library; if not, write to the
13
# Free Software Foundation, Inc.,
14
# 59 Temple Place, Suite 330,
15
# Boston, MA 02111-1307 USA
17
# This file is part of urlgrabber, a high-level cross-protocol url-grabber
18
# Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
20
# $Id: progress.py,v 1.5 2005/01/14 18:21:41 rtomayko Exp $
29
self.update_period = 0.3 # seconds
36
self.start_time = None
37
self.last_amount_read = 0
38
self.last_update_time = None
39
self.re = RateEstimator()
41
def start(self, filename=None, url=None, basename=None,
42
size=None, now=None, text=None):
43
self.filename = filename
45
self.basename = basename
48
#size = None ######### TESTING
50
if not size is None: self.fsize = format_number(size) + 'B'
52
if now is None: now = time.time()
54
self.re.start(size, now)
55
self.last_amount_read = 0
56
self.last_update_time = now
59
def _do_start(self, now=None):
62
def update(self, amount_read, now=None):
63
# for a real gui, you probably want to override and put a call
64
# to your mainloop iteration function here
65
if now is None: now = time.time()
66
if (now >= self.last_update_time + self.update_period) or \
67
not self.last_update_time:
68
self.re.update(amount_read, now)
69
self.last_amount_read = amount_read
70
self.last_update_time = now
71
self._do_update(amount_read, now)
73
def _do_update(self, amount_read, now=None):
76
def end(self, amount_read, now=None):
77
if now is None: now = time.time()
78
self.re.update(amount_read, now)
79
self.last_amount_read = amount_read
80
self.last_update_time = now
81
self._do_end(amount_read, now)
83
def _do_end(self, amount_read, now=None):
86
class TextMeter(BaseMeter):
87
def __init__(self, fo=sys.stderr):
88
BaseMeter.__init__(self)
91
def _do_update(self, amount_read, now=None):
92
etime = self.re.elapsed_time()
93
fetime = format_time(etime)
94
fread = format_number(amount_read)
96
if self.text is not None:
100
if self.size is None:
101
out = '\r%-60.60s %5sB %s ' % \
102
(text, fread, fetime)
104
rtime = self.re.remaining_time()
105
frtime = format_time(rtime)
106
frac = self.re.fraction_read()
107
bar = '='*int(25 * frac)
109
out = '\r%-25.25s %3i%% |%-25.25s| %5sB %8s ETA ' % \
110
(text, frac*100, bar, fread, frtime)
115
def _do_end(self, amount_read, now=None):
116
total_time = format_time(self.re.elapsed_time())
117
total_size = format_number(amount_read)
118
if self.text is not None:
122
if self.size is None:
123
out = '\r%-60.60s %5sB %s ' % \
124
(text, total_size, total_time)
127
out = '\r%-25.25s %3i%% |%-25.25s| %5sB %8s ' % \
128
(text, 100, bar, total_size, total_time)
129
self.fo.write(out + '\n')
132
text_progress_meter = TextMeter
134
class MultiFileHelper(BaseMeter):
135
def __init__(self, master):
136
BaseMeter.__init__(self)
139
def _do_start(self, now):
140
self.master.start_meter(self, now)
142
def _do_update(self, amount_read, now):
143
# elapsed time since last update
144
self.master.update_meter(self, now)
146
def _do_end(self, amount_read, now):
147
self.ftotal_time = format_time(now - self.start_time)
148
self.ftotal_size = format_number(self.last_amount_read)
149
self.master.end_meter(self, now)
151
def failure(self, message, now=None):
152
self.master.failure_meter(self, message, now)
154
def message(self, message):
155
self.master.message_meter(self, message)
157
class MultiFileMeter:
158
helperclass = MultiFileHelper
161
self.in_progress_meters = []
162
self._lock = thread.allocate_lock()
163
self.update_period = 0.3 # seconds
166
self.finished_files = 0
167
self.failed_files = 0
169
self.total_size = None
171
self.start_time = None
172
self.finished_file_size = 0
173
self.last_update_time = None
174
self.re = RateEstimator()
176
def start(self, numfiles=None, total_size=None, now=None):
177
if now is None: now = time.time()
178
self.numfiles = numfiles
179
self.finished_files = 0
180
self.failed_files = 0
182
self.total_size = total_size
184
self.start_time = now
185
self.finished_file_size = 0
186
self.last_update_time = now
187
self.re.start(total_size, now)
190
def _do_start(self, now):
193
def end(self, now=None):
194
if now is None: now = time.time()
197
def _do_end(self, now):
200
def lock(self): self._lock.acquire()
201
def unlock(self): self._lock.release()
203
###########################################################
204
# child meter creation and destruction
206
newmeter = self.helperclass(self)
207
self.meters.append(newmeter)
210
def removeMeter(self, meter):
211
self.meters.remove(meter)
213
###########################################################
214
# child functions - these should only be called by helpers
215
def start_meter(self, meter, now):
216
if not meter in self.meters:
217
raise ValueError('attempt to use orphaned meter')
220
if not meter in self.in_progress_meters:
221
self.in_progress_meters.append(meter)
225
self._do_start_meter(meter, now)
227
def _do_start_meter(self, meter, now):
230
def update_meter(self, meter, now):
231
if not meter in self.meters:
232
raise ValueError('attempt to use orphaned meter')
233
if (now >= self.last_update_time + self.update_period) or \
234
not self.last_update_time:
235
self.re.update(self._amount_read(), now)
236
self.last_update_time = now
237
self._do_update_meter(meter, now)
239
def _do_update_meter(self, meter, now):
242
def end_meter(self, meter, now):
243
if not meter in self.meters:
244
raise ValueError('attempt to use orphaned meter')
247
try: self.in_progress_meters.remove(meter)
248
except ValueError: pass
250
self.finished_files += 1
251
self.finished_file_size += meter.last_amount_read
254
self._do_end_meter(meter, now)
256
def _do_end_meter(self, meter, now):
259
def failure_meter(self, meter, message, now):
260
if not meter in self.meters:
261
raise ValueError('attempt to use orphaned meter')
264
try: self.in_progress_meters.remove(meter)
265
except ValueError: pass
267
self.failed_files += 1
268
if meter.size and self.failed_size is not None:
269
self.failed_size += meter.size
271
self.failed_size = None
274
self._do_failure_meter(meter, message, now)
276
def _do_failure_meter(self, meter, message, now):
279
def message_meter(self, meter, message):
282
########################################################
284
def _amount_read(self):
285
tot = self.finished_file_size
286
for m in self.in_progress_meters:
287
tot += m.last_amount_read
291
class TextMultiFileMeter(MultiFileMeter):
292
def __init__(self, fo=sys.stderr):
294
MultiFileMeter.__init__(self)
296
# files: ###/### ###% data: ######/###### ###% time: ##:##:##/##:##:##
297
def _do_update_meter(self, meter, now):
300
format = "files: %3i/%-3i %3i%% data: %6.6s/%-6.6s %3i%% " \
302
df = self.finished_files
303
tf = self.numfiles or 1
304
pf = 100 * float(df)/tf + 0.49
305
dd = self.re.last_amount_read
307
pd = 100 * (self.re.fraction_read() or 0) + 0.49
308
dt = self.re.elapsed_time()
309
rt = self.re.remaining_time()
310
if rt is None: tt = None
313
fdd = format_number(dd) + 'B'
314
ftd = format_number(td) + 'B'
315
fdt = format_time(dt, 1)
316
ftt = format_time(tt, 1)
318
out = '%-79.79s' % (format % (df, tf, pf, fdd, ftd, pd, fdt, ftt))
319
self.fo.write('\r' + out)
324
def _do_end_meter(self, meter, now):
327
format = "%-30.30s %6.6s %8.8s %9.9s"
329
size = meter.last_amount_read
330
fsize = format_number(size) + 'B'
331
et = meter.re.elapsed_time()
332
fet = format_time(et, 1)
333
frate = format_number(size / et) + 'B/s'
335
out = '%-79.79s' % (format % (fn, fsize, fet, frate))
336
self.fo.write('\r' + out + '\n')
339
self._do_update_meter(meter, now)
341
def _do_failure_meter(self, meter, message, now):
344
format = "%-30.30s %6.6s %s"
346
if type(message) in (type(''), type(u'')):
347
message = message.splitlines()
348
if not message: message = ['']
349
out = '%-79s' % (format % (fn, 'FAILED', message[0] or ''))
350
self.fo.write('\r' + out + '\n')
351
for m in message[1:]: self.fo.write(' ' + m + '\n')
354
self._do_update_meter(meter, now)
356
def message_meter(self, meter, message):
363
def _do_end(self, now):
364
self._do_update_meter(None, now)
372
######################################################################
373
# support classes and functions
376
def __init__(self, timescale=5.0):
377
self.timescale = timescale
379
def start(self, total=None, now=None):
380
if now is None: now = time.time()
382
self.start_time = now
383
self.last_update_time = now
384
self.last_amount_read = 0
387
def update(self, amount_read, now=None):
388
if now is None: now = time.time()
390
# if we just started this file, all bets are off
391
self.last_update_time = now
392
self.last_amount_read = 0
396
#print 'times', now, self.last_update_time
397
time_diff = now - self.last_update_time
398
read_diff = amount_read - self.last_amount_read
399
self.last_update_time = now
400
self.last_amount_read = amount_read
401
self.ave_rate = self._temporal_rolling_ave(\
402
time_diff, read_diff, self.ave_rate, self.timescale)
403
#print 'results', time_diff, read_diff, self.ave_rate
405
#####################################################################
407
def average_rate(self):
408
"get the average transfer rate (in bytes/second)"
411
def elapsed_time(self):
412
"the time between the start of the transfer and the most recent update"
413
return self.last_update_time - self.start_time
415
def remaining_time(self):
416
"estimated time remaining"
417
if not self.ave_rate or not self.total: return None
418
return (self.total - self.last_amount_read) / self.ave_rate
420
def fraction_read(self):
421
"""the fraction of the data that has been read
422
(can be None for unknown transfer size)"""
423
if self.total is None: return None
424
elif self.total == 0: return 1.0
425
else: return float(self.last_amount_read)/self.total
427
#########################################################################
429
def _temporal_rolling_ave(self, time_diff, read_diff, last_ave, timescale):
430
"""a temporal rolling average performs smooth averaging even when
431
updates come at irregular intervals. This is performed by scaling
432
the "epsilon" according to the time since the last update.
433
Specifically, epsilon = time_diff / timescale
435
As a general rule, the average will take on a completely new value
436
after 'timescale' seconds."""
437
epsilon = time_diff / timescale
438
if epsilon > 1: epsilon = 1.0
439
return self._rolling_ave(time_diff, read_diff, last_ave, epsilon)
441
def _rolling_ave(self, time_diff, read_diff, last_ave, epsilon):
442
"""perform a "rolling average" iteration
443
a rolling average "folds" new data into an existing average with
444
some weight, epsilon. epsilon must be between 0.0 and 1.0 (inclusive)
445
a value of 0.0 means only the old value (initial value) counts,
446
and a value of 1.0 means only the newest value is considered."""
449
recent_rate = read_diff / time_diff
450
except ZeroDivisionError:
452
if last_ave is None: return recent_rate
453
elif recent_rate is None: return last_ave
455
# at this point, both last_ave and recent_rate are numbers
456
return epsilon * recent_rate + (1 - epsilon) * last_ave
458
def _round_remaining_time(self, rt, start_time=15.0):
459
"""round the remaining time, depending on its size
460
If rt is between n*start_time and (n+1)*start_time round downward
461
to the nearest multiple of n (for any counting number n).
462
If rt < start_time, round down to the nearest 1.
463
For example (for start_time = 15.0):
471
if rt < 0: return 0.0
472
shift = int(math.log(rt/start_time)/math.log(2))
474
if shift <= 0: return rt
475
return float(int(rt) >> shift << shift)
478
def format_time(seconds, use_hours=0):
479
if seconds is None or seconds < 0:
480
if use_hours: return '--:--:--'
483
seconds = int(seconds)
484
minutes = seconds / 60
485
seconds = seconds % 60
488
minutes = minutes % 60
489
return '%02i:%02i:%02i' % (hours, minutes, seconds)
491
return '%02i:%02i' % (minutes, seconds)
493
def format_number(number, SI=0, space=' '):
494
"""Turn numbers into human-readable metric-like numbers"""
495
symbols = ['', # (none)
511
# we want numbers between
512
while number > thresh:
514
number = number / step
516
# just in case someone needs more than 1000 yottabytes!
517
diff = depth - len(symbols) + 1
520
number = number * thresh**depth
522
if type(number) == type(1) or type(number) == type(1L):
525
# must use 9.95 for proper sizing. For example, 9.99 will be
526
# rounded to 10.0 with the .1f format string (which is too long)
531
return(format % (float(number or 0), space, symbols[depth]))