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]))