2021-06-09 01:31:44 +03:00
# This work is licensed under the GNU GPLv2 or later.
# See the COPYING file in the top-level directory.
# This file is part of urlgrabber, a high-level cross-protocol url-grabber
# Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko
# This code is all straight from python-urlgrabber, which we historically
# used the system installed version of. But since the project is in
# maintenance mode upstream, and eventually we want to switch to python3,
# we are just copying this for now.
import fcntl
import struct
2021-06-09 21:29:08 +03:00
import sys
2021-06-09 01:31:44 +03:00
import termios
2021-06-09 21:29:08 +03:00
import time
2021-06-09 01:31:44 +03:00
# Code from https://mail.python.org/pipermail/python-list/2000-May/033365.html
def terminal_width ( fd = 1 ) :
""" Get the real terminal width """
try :
buf = ' abcdefgh '
buf = fcntl . ioctl ( fd , termios . TIOCGWINSZ , buf )
2021-06-09 21:29:08 +03:00
ret = struct . unpack ( ' hhhh ' , buf ) [ 1 ] # pragma: no cover
return ret or 80 # pragma: no cover
2021-06-09 01:31:44 +03:00
except IOError :
return 80
_term_width_val = None
_term_width_last = None
def terminal_width_cached ( fd = 1 , cache_timeout = 1.000 ) :
""" Get the real terminal width, but cache it for a bit. """
global _term_width_val
global _term_width_last
now = time . time ( )
if _term_width_val is None or ( now - _term_width_last ) > cache_timeout :
_term_width_val = terminal_width ( fd )
_term_width_last = now
return _term_width_val
class TerminalLine :
""" Help create dynamic progress bars, uses terminal_width_cached(). """
def __init__ ( self , min_rest = 0 , beg_len = None , fd = 1 , cache_timeout = 1.000 ) :
if beg_len is None :
beg_len = min_rest
self . _min_len = min_rest
self . llen = max ( terminal_width_cached ( fd , cache_timeout ) , beg_len )
self . _fin = False
def __len__ ( self ) :
""" Usable length for elements. """
return self . llen - self . _min_len
def rest_split ( self , fixed , elements = 2 ) :
""" After a fixed length, split the rest of the line length among
a number of different elements ( default = 2 ) . """
2021-06-09 21:29:08 +03:00
return max ( self . llen - fixed , 0 ) / / elements
2021-06-09 01:31:44 +03:00
def add ( self , element , full_len = None ) :
""" If there is room left in the line, above min_len, add element.
Note that as soon as one add fails all the rest will fail too . """
if full_len is None :
full_len = len ( element )
if len ( self ) < full_len :
self . _fin = True
if self . _fin :
return ' '
self . llen - = len ( element )
return element
def rest ( self ) :
""" Current rest of line, same as .rest_split(fixed=0, elements=1). """
return self . llen
class BaseMeter :
def __init__ ( self ) :
self . update_period = 0.3 # seconds
self . text = None
self . size = None
self . start_time = None
self . last_amount_read = 0
self . last_update_time = None
self . re = RateEstimator ( )
def start ( self , text , size ) :
self . text = text
self . size = size
2021-06-09 21:29:08 +03:00
assert type ( size ) in [ int , type ( None ) ]
assert self . text is not None
2021-06-09 01:31:44 +03:00
now = time . time ( )
self . start_time = now
self . re . start ( size , now )
self . last_amount_read = 0
self . last_update_time = now
2021-06-09 21:29:08 +03:00
def update ( self , amount_read ) :
2021-06-09 01:31:44 +03:00
# for a real gui, you probably want to override and put a call
# to your mainloop iteration function here
2021-06-09 21:29:08 +03:00
assert type ( amount_read ) is int
now = time . time ( )
2022-12-14 20:57:10 +03:00
self . last_amount_read = amount_read
self . re . update ( amount_read , now )
2021-06-09 01:31:44 +03:00
if ( not self . last_update_time or
( now > = self . last_update_time + self . update_period ) ) :
self . last_update_time = now
2021-06-09 21:29:08 +03:00
self . _do_update ( amount_read )
2021-06-09 01:31:44 +03:00
2021-06-09 21:29:08 +03:00
def _do_update ( self , amount_read ) :
2021-06-09 01:31:44 +03:00
pass
def end ( self ) :
2021-06-09 21:29:08 +03:00
self . _do_end ( )
2021-06-09 01:31:44 +03:00
2021-06-09 21:29:08 +03:00
def _do_end ( self ) :
2021-06-09 01:31:44 +03:00
pass
#
# update: No size (minimal: 17 chars)
# -----------------------------------
# <text> <rate> | <current size> <elapsed time>
# 8-48 1 8 3 6 1 9 5
#
# Order: 1. <text>+<current size> (17)
# 2. +<elapsed time> (10, total: 27)
# 3. + ( 5, total: 32)
# 4. +<rate> ( 9, total: 41)
#
# update: Size, Single file
# -------------------------
# <text> <pc> <bar> <rate> | <current size> <eta time> ETA
# 8-25 1 3-4 1 6-16 1 8 3 6 1 9 1 3 1
#
# Order: 1. <text>+<current size> (17)
# 2. +<eta time> (10, total: 27)
# 3. +ETA ( 5, total: 32)
# 4. +<pc> ( 4, total: 36)
# 5. +<rate> ( 9, total: 45)
# 6. +<bar> ( 7, total: 52)
#
# update: Size, All files
# -----------------------
# <text> <total pc> <pc> <bar> <rate> | <current size> <eta time> ETA
# 8-22 1 5-7 1 3-4 1 6-12 1 8 3 6 1 9 1 3 1
#
# Order: 1. <text>+<current size> (17)
# 2. +<eta time> (10, total: 27)
# 3. +ETA ( 5, total: 32)
# 4. +<total pc> ( 5, total: 37)
# 4. +<pc> ( 4, total: 41)
# 5. +<rate> ( 9, total: 50)
# 6. +<bar> ( 7, total: 57)
#
# end
# ---
# <text> | <current size> <elapsed time>
# 8-56 3 6 1 9 5
#
# Order: 1. <text> ( 8)
# 2. +<current size> ( 9, total: 17)
# 3. +<elapsed time> (10, total: 27)
# 4. + ( 5, total: 32)
#
def _term_add_bar ( tl , bar_max_length , pc ) :
bar_len = bar_max_length * pc
ibar_len = int ( bar_len )
progressbar = ' = ' * ibar_len
if ( bar_len - ibar_len ) > = 0.5 :
progressbar + = ' - '
return tl . add ( ' [ %-*.*s ] ' % ( bar_max_length , bar_max_length ,
progressbar ) )
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 , output = sys . stderr ) :
BaseMeter . __init__ ( self )
self . output = output
2021-06-09 21:29:08 +03:00
def _do_update ( self , amount_read ) :
2021-06-09 01:31:44 +03:00
etime = self . re . elapsed_time ( )
fread = format_number ( amount_read )
ave_dl = format_number ( self . re . average_rate ( ) )
# Include text + ui_rate in minimal
tl = TerminalLine ( 8 , 8 + 1 + 8 )
# For big screens, make it more readable.
use_hours = bool ( tl . llen > 80 )
ui_size = tl . add ( ' | %5s B ' % fread )
if self . size is None :
ui_time = tl . add ( ' %s ' % format_time ( etime , use_hours ) )
ui_end = tl . add ( ' ' * 5 )
ui_rate = tl . add ( ' %5s B/s ' % ave_dl )
2021-06-09 21:29:08 +03:00
out = ' %-*.*s %s %s %s %s \r ' % ( tl . rest ( ) , tl . rest ( ) , self . text ,
2021-06-09 01:31:44 +03:00
ui_rate , ui_size , ui_time , ui_end )
else :
rtime = self . re . remaining_time ( )
frtime = format_time ( rtime , use_hours )
frac = self . re . fraction_read ( )
ui_time = tl . add ( ' %s ' % frtime )
ui_end = tl . add ( ' ETA ' )
ui_pc = tl . add ( ' %2i %% ' % ( frac * 100 ) )
ui_rate = tl . add ( ' %5s B/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 )
2021-06-09 21:29:08 +03:00
out = ' \r %-*.*s %s %s %s %s %s %s \r ' % (
tl . rest ( ) , tl . rest ( ) , self . text ,
ui_pc , ui_bar ,
2021-06-09 01:31:44 +03:00
ui_rate , ui_size , ui_time , ui_end
)
self . output . write ( out )
self . output . flush ( )
2021-06-09 21:29:08 +03:00
def _do_end ( self ) :
amount_read = self . last_amount_read
2021-06-09 01:31:44 +03:00
total_size = format_number ( amount_read )
tl = TerminalLine ( 8 )
# For big screens, make it more readable.
use_hours = bool ( tl . llen > 80 )
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 )
2022-11-09 12:33:57 +03:00
if not not_done and amount_read == 0 :
# Doesn't need to print total_size
ui_size = tl . add ( ' | %5s ' % ' ' )
else :
ui_size = tl . add ( ' | %5s B ' % total_size )
2021-06-09 21:29:08 +03:00
out = ' \r %-*.*s %s %s %s \n ' % ( tl . rest ( ) , tl . rest ( ) , self . text ,
2021-06-09 01:31:44 +03:00
ui_size , ui_time , ui_end )
self . output . write ( out )
self . output . flush ( )
######################################################################
# support classes and functions
class RateEstimator :
def __init__ ( self , timescale = 5.0 ) :
self . timescale = timescale
self . total = None
self . start_time = None
self . last_update_time = None
self . last_amount_read = 0
self . ave_rate = None
2021-06-09 21:29:08 +03:00
def start ( self , total , now ) :
2021-06-09 01:31:44 +03:00
self . total = total
self . start_time = now
self . last_update_time = now
self . last_amount_read = 0
self . ave_rate = None
2021-06-09 21:29:08 +03:00
def update ( self , amount_read , now ) :
2021-06-09 01:31:44 +03:00
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 = amount_read
self . ave_rate = None
return
time_diff = now - self . last_update_time
read_diff = amount_read - self . last_amount_read
# 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 (
time_diff , read_diff , self . ave_rate , self . timescale )
self . last_amount_read = amount_read
#####################################################################
# result methods
def average_rate ( self ) :
" get the average transfer rate (in bytes/second) "
return self . ave_rate
def elapsed_time ( self ) :
" the time between the start of the transfer and the most recent update "
return self . last_update_time - self . start_time
def remaining_time ( self ) :
" estimated time remaining "
if not self . ave_rate or not self . total :
return None
return ( self . total - self . last_amount_read ) / self . ave_rate
def fraction_read ( self ) :
""" the fraction of the data that has been read
( can be None for unknown transfer size ) """
if self . total is None :
return None
2021-06-09 21:29:08 +03:00
if self . total == 0 :
return 1.0 # pragma: no cover
return float ( self . last_amount_read ) / self . total
2021-06-09 01:31:44 +03:00
#########################################################################
# support methods
def _temporal_rolling_ave ( self , time_diff , read_diff , last_ave , timescale ) :
""" a temporal rolling average performs smooth averaging even when
updates come at irregular intervals . This is performed by scaling
the " epsilon " according to the time since the last update .
Specifically , epsilon = time_diff / timescale
As a general rule , the average will take on a completely new value
after ' timescale ' seconds . """
epsilon = min ( time_diff / timescale , 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
2021-06-09 21:29:08 +03:00
except ZeroDivisionError : # pragma: no cover
2021-06-09 01:31:44 +03:00
recent_rate = None
if last_ave is None :
return recent_rate
2021-06-09 21:29:08 +03:00
if recent_rate is None :
return last_ave # pragma: no cover
2021-06-09 01:31:44 +03:00
# at this point, both last_ave and recent_rate are numbers
return epsilon * recent_rate + ( 1 - epsilon ) * last_ave
def format_time ( seconds , use_hours = 0 ) :
if seconds is None or seconds < 0 :
if use_hours :
return ' --:--:-- '
else :
return ' --:-- '
elif seconds == float ( ' inf ' ) :
2021-06-09 21:29:08 +03:00
return ' Infinite ' # pragma: no cover
2021-06-09 01:31:44 +03:00
else :
seconds = int ( seconds )
minutes = seconds / / 60
seconds = seconds % 60
if use_hours :
hours = minutes / / 60
minutes = minutes % 60
return ' %02i : %02i : %02i ' % ( hours , minutes , seconds )
else :
return ' %02i : %02i ' % ( minutes , seconds )
2021-06-09 21:29:08 +03:00
def format_number ( number ) :
2021-06-09 01:31:44 +03:00
""" Turn numbers into human-readable metric-like numbers """
symbols = [ ' ' , # (none)
' k ' , # kilo
' M ' , # mega
' G ' , # giga
' T ' , # tera
' P ' , # peta
' E ' , # exa
' Z ' , # zetta
' Y ' ] # yotta
2021-06-09 21:29:08 +03:00
step = 1024.0
2021-06-09 01:31:44 +03:00
thresh = 999
depth = 0
max_depth = len ( symbols ) - 1
number = number or 0
# 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.
while number > thresh and depth < max_depth :
depth = depth + 1
number = number / step
if isinstance ( number , int ) :
# it's an int or a long, which means it didn't get divided,
# which means it's already short enough
fmt = ' %i %s %s '
elif number < 9.95 :
# must use 9.95 for proper sizing. For example, 9.99 will be
# rounded to 10.0 with the .1f format string (which is too long)
fmt = ' %.1f %s %s '
else :
fmt = ' %.0f %s %s '
2021-06-09 21:29:08 +03:00
return fmt % ( float ( number or 0 ) , " " , symbols [ depth ] )