Skip to content

Monitoring download progress. #1208

Closed
Closed

Description

@lovelydinosaur

For clients that would like to display something like a progress bar for downloads, there's an issue in that while you're streaming data from iter_content(), you can't properly know how far you have progressed through the download because we'll often be performing gzip decompression. The Content-Length header gives the length of the raw data. (And we really don't want to be streaming from iter_raw() because we really do want the uncompressed bytes, not the bytes on the wire.)

See also for example, this question related to requests... https://stackoverflow.com/questions/21778885/python3-progress-bar-and-download-with-gzip

It might make sense to provide an API for monitoring the number of bytes that have been consumed by the underlying iter_raw calls, for example something like... response.num_bytes_downloaded.

with open("download.txt", "wb") as download_file:
    with httpx.stream('http://www.example.org') as response:
        content_length = response.headers.get("Content-Length")
        for chunk in response.iter_content():
            download_file.write(chunk)
            if content_length is not None:
                percent_complete = 100.0 * (response.num_bytes_downloaded / content_length)
                print(f"{percent_complete:.2f}%")

Perhaps we ought to provide a response.percent_complete that wraps this up nicely, returning either a 0.0 <= float <= 100.0 or None if the Content-Length is unknown, so...

with open("download.txt", "wb") as download_file:
    with httpx.stream('http://www.example.org') as response:
        for chunk in response.iter_content():
            download_file.write(chunk)
            if response.percent_complete is not None:
                print(f"{response.percent_complete:.2f}%")

Activity

  1. lovelydinosaur commented on Aug 21, 2020

    @lovelydinosaur
    MemberAuthor

    For requests it looks like this interface is possibly available via request.raw.tell()... https://urllib3.readthedocs.io/en/latest/reference/#urllib3.response.HTTPResponse.tell

  2. lovelydinosaur commented on Aug 21, 2020

    @lovelydinosaur
    MemberAuthor

    Any API decision here might be influenced by if we want to expose the raw stream instance on the response or not.

  3. florimondmanca commented on Sep 1, 2020

    @florimondmanca
    Contributor

    Regardless of the API we decide on, one way to challenge it and make sure it's right enough for most use cases is to see if it would integrate well with tqdm...

    tqdm supports a manual .update(<increment>)-based usage that was specifically designed for this kind of "read from a known-length stream" use cases.

    The subtle thing here is that we can't just naively pass .update(len(chunk)) since chunk is decoded data, while we want to do .update(<raw chunk size>).

    Since tqdm expects an increment, I guess one sensible thing to do would be to also add a response.last_raw_chunk_size attribute (or drop response.num_bytes_downloaded entirely, since we can accumulate last raw chunk sizes to get the total number of downloaded bytes anyway — but computing the diff manually is cumbersome).

    For example:

    import httpx
    from tqdm import tqdm
    
    with open("download.txt", "wb") as download_file:
        data = b"*" * 1000000
        with httpx.stream("POST", "https://httpbin.org/anything", data=data) as response:
            content_length = int(response.headers["Content-Length"])
            with tqdm(total=content_length) as progress:
                for chunk in response.iter_bytes():
                    download_file.write(chunk)
                    progress.update(response.last_raw_chunk_size)

    Example in-progress output (obtained by using progress.update(len(chunk)), just for demo purposes):

     32%|█████████████████████████                                                      | 317352/1000431 [00:00<00:00, 922202.25it/s]

    And here's Tom's percent_complete example using this alternative response.last_raw_chunk_size API:

    with open("download.txt", "wb") as download_file:
        with httpx.stream('http://www.example.org') as response:
            content_length = int(response.headers["Content-Length"])
            num_bytes_downloaded = 0
            for chunk in response.iter_content():
                download_file.write(chunk)
                num_bytes_downloaded += response.last_raw_chunk_size
                percent_complete = 100.0 * (num_bytes_downloaded / content_length)
                print(f"{percent_complete:.2f}%")
  4. cdeler commented on Sep 10, 2020

    @cdeler
    Contributor

    @tomchristie
    can we treat this issue as closed after #1268 ?

  5. lovelydinosaur commented on Sep 10, 2020

    @lovelydinosaur
    MemberAuthor

    Yup! 👍👍👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions