Elegant Output¶
We are finally in the last lesson. Let’s make our output beautiful.
Our goal will be to make the output look like this:
<state> | <File size> | <Downloaded> | <speed> [<progress bar>] <percentage>% <eta>
We have all the information in 1 line seperated by a pipe character “|”. The
state will show us in real time if there is any error. The file size, downloaded
bytes and speed will be in human readable form so that we can easily read it.
The progress bar will indicate how much portion we have downloaded so far. The
percentage will indicate the same as the progress bar but in numbers. Finally,
eta
is the estimated time to finish the download. The information will be
printed in only 1 line. If it changes, we will make the information be updated
in the same line instead of printing so many lines like we did in the
on_speed_changed
callback.
Showing information in one line¶
First, instead of having a callback function for each signal, let’s make 1
callback that will update all the information whenever 1 thing changes. Let’s
remove on_size_changed
, on_speed_changed
and on_state_changed
callbacks and write 1 callback to print the state
, size
, downloaded
,
and speed
instead:
def on_anything_changed(downloader, old_state=None):
state = downloader.state
size = '{} {}'.format(*downloader.human_size)
downloaded = '{} {}'.format(*downloader.human_downloaded)
speed = '{} {}'.format(*downloader.human_speed)
text = '{} | {} | {} | {}'.format(state, size, downloaded, speed)
print(text)
We will do the progress bar, the percentage and the estimated download time in a later section.
Next, we modify all Downloader.listen()
calls to register the new function:
#listen to everything
dl.listen('size-changed', on_anything_changed)
dl.listen('speed-changed', on_anything_changed)
dl.listen('state-changed', on_anything_changed)
Now our callback will be called when the state changes, when we know the
size and periodically when speed-change
signal is emitted. Notice that we
also printed number of bytes downloaded
which we did not do in previous
lessons. Now our output will be something like this:
pause | 9.865234375 KB | 0 B | 0 B/s
start | 9.865234375 KB | 0 B | 0 B/s
start | 9.865234375 KB | 0 B | 0 B/s
start | 9.865234375 KB | 2.0 KB | 1.9990254031527928 KB/s
start | 9.865234375 KB | 4.0 KB | 1.9989961600987338 KB/s
start | 9.865234375 KB | 6.0 KB | 1.9988544205541783 KB/s
start | 9.865234375 KB | 8.0 KB | 1.9987502773920875 KB/s
start | 9.865234375 KB | 9.865234375 KB | 1.9987502773920875 KB/s
complete | 9.865234375 KB | 9.865234375 KB | 0 B/s
complete | 9.865234375 KB | 9.865234375 KB | 0 B/s
Ok. We still have ugly output. First, let’s make all numbers rounded to 2
decimal places. In the callback, we will modify our format strings
:
state = downloader.state
size = '{:0.2f} {}'.format(*downloader.human_size)
downloaded = '{:0.2f} {}'.format(*downloader.human_downloaded)
speed = '{:0.2f} {}'.format(*downloader.human_speed)
Second, we do not want to print multiple lines. We want to print only 1 line.
Let’s use the print
function arguments to stay on the same line and use the
character \r
to update it:
text = '\r{} | {} | {} | {}'.format(state, size, downloaded, speed)
print(text, end='', flush=True)
Now our callback will not print many lines. Instead, it will go back to the beginning of the line and print the information on the same line erasing anything previously shown.
Furthermore, let’s modify the print
call to print spaces to fill all the
line with 79 characters just to erase the whole line in case we have garbage out
of our text width:
print(text.ljust(79), end='', flush=True)
Our callback now becomes:
def on_anything_changed(downloader, old_state=None):
state = downloader.state
size = '{:0.2f} {}'.format(*downloader.human_size)
downloaded = '{:0.2f} {}'.format(*downloader.human_downloaded)
speed = '{:0.2f} {}'.format(*downloader.human_speed)
text = '\r{} | {} | {} | {}'.format(state, size, downloaded, speed)
print(text.ljust(79), end='', flush=True)
Showing the progress bar, percentage and ETA¶
Let’s start with the progress bar. We use Downlaoder.bar()
function to
generate a progress bar. The function takes 2 optional arguments. The first is
width
which is the length in characters of the progress bar. It defaults to
30. Let’s make it 10. The second is char
which is the character to
use to fill the bar. It defaults to ‘=’. Let’s make this a dash instead:
bar = downloader.bar(width=10, char='-')
Our callback now becomes:
def on_anything_changed(downloader, old_state=None):
state = downloader.state
size = '{:0.2f} {}'.format(*downloader.human_size)
downloaded = '{:0.2f} {}'.format(*downloader.human_downloaded)
speed = '{:0.2f} {}'.format(*downloader.human_speed)
bar = downloader.bar(width=10, char='-')
text = '\r{} | {} | {} | {} [{}]'.format(
state,
size,
downloaded,
speed,
bar
)
print(text.ljust(79), end='', flush=True)
Notice how we enclosed the progress bar in brackes within our format string.
Percentage and ETA are straight forward. We use Downloader.percentage
and
Downloader.eta
properties of the downloader:
percentage = int(downloader.percentage)
eta = downloader.eta
Downloader.percentage
property returns the percentage (from 0 to 100) as a
float
. we converted it to int
to remove any digits after the decimal
point to reduce user confusion. eta returns a datetime.timedelta
instance
which tells us the estimated time remaining until the download is completed.
Now our full callback function becomes:
def on_anything_changed(downloader, old_state=None):
state = downloader.state
size = '{:0.2f} {}'.format(*downloader.human_size)
downloaded = '{:0.2f} {}'.format(*downloader.human_downloaded)
speed = '{:0.2f} {}'.format(*downloader.human_speed)
bar = downloader.bar(width=10, char='-')
percentage = int(downloader.percentage)
eta = downloader.eta
text = '\r{} | {} | {} | {} [{}] {}% {}'.format(
state,
size,
downloaded,
speed,
bar,
percentage,
eta
)
print(text.ljust(79), end='', flush=True)
And now, this is our awesome program:
import bitpit
import pathlib
def on_anything_changed(downloader, old_state=None):
state = downloader.state
size = '{:0.2f} {}'.format(*downloader.human_size)
downloaded = '{:0.2f} {}'.format(*downloader.human_downloaded)
speed = '{:0.2f} {}'.format(*downloader.human_speed)
bar = downloader.bar(width=10, char='-')
percentage = int(downloader.percentage)
eta = downloader.eta
text = '\r{} | {} | {} | {} [{}] {}% {}'.format(
state,
size,
downloaded,
speed,
bar,
percentage,
eta
)
print(text.ljust(79), end='', flush=True)
#will download this
url = 'https://www.python.org/static/img/python-logo.png'
#this is our downloader
dl = bitpit.Downloader(
url,
path=pathlib.Path.home() / 'Desktop' / 'logo.png',
restart_wait=30,
rate_limit=2048,
timeout=60,
chunk_size=1024
)
#listen to everything
dl.listen('size-changed', on_anything_changed)
dl.listen('speed-changed', on_anything_changed)
dl.listen('state-changed', on_anything_changed)
#start downloading and tell user download has started.
dl.start()
print('Download has started.')
#end of the main thread
The output I got from this program is below:
start | 9.87 KB | 4.00 KB | 2.00 KB/s [---- ] 40% 0:00:02.934069
You can see that fractions of a second are shown in eta
which is not very
nice. However, I will leave this to you to fix.
Finally we have an awesome download program. Of course, there are many things we can improve on it. But I believe this form is enough to explain bitpit features and how to use it.
You may want to have a look at bitpit Reference for complete documentation of the library.
THE END…