11import os
22from pathlib import Path
33import platform
4- from typing import Any , AnyStr , Iterator , List , Optional , Tuple , Union
4+ from types import TracebackType
5+ from typing import Any , AnyStr , Callable , Iterator , List , Optional , Tuple , Type , Union
56from warnings import warn
67from packaging .utils import canonicalize_name as normalize
78import requests
89from . import PYPI_SIMPLE_ENDPOINT , __url__ , __version__
910from .classes import DistributionPackage , IndexPage , ProjectPage
1011from .parse_repo import parse_repo_index_response , parse_repo_project_response
1112from .parse_stream import parse_links_stream_response
13+ from .progress import ProgressTracker , null_progress_tracker
1214from .util import AbstractDigestChecker , DigestChecker , NullDigestChecker
1315
1416#: The User-Agent header used for requests; not used when the user provides eir
@@ -96,7 +98,12 @@ def __init__(
9698 def __enter__ (self ) -> "PyPISimple" :
9799 return self
98100
99- def __exit__ (self , _exc_type : Any , _exc_value : Any , _exc_tb : Any ) -> None :
101+ def __exit__ (
102+ self ,
103+ _exc_type : Optional [Type [BaseException ]],
104+ _exc_val : Optional [BaseException ],
105+ _exc_tb : Optional [TracebackType ],
106+ ) -> None :
100107 self .s .close ()
101108
102109 def get_index_page (
@@ -215,6 +222,7 @@ def download_package(
215222 path : Union [AnyStr , "os.PathLike[AnyStr]" ],
216223 verify : bool = True ,
217224 keep_on_error : bool = False ,
225+ progress : Optional [Callable [[Optional [int ]], ProgressTracker ]] = None ,
218226 timeout : Union [float , Tuple [float , float ], None ] = None ,
219227 ) -> None :
220228 """
@@ -225,6 +233,13 @@ def download_package(
225233 If an error occurs while downloading or verifying digests, and
226234 ``keep_on_error`` is not true, the downloaded file is not saved.
227235
236+ Download progress can be tracked (e.g., for display by a progress bar)
237+ by passing an appropriate callable as the ``progress`` argument. This
238+ callable will be passed the length of the downloaded file, if known,
239+ and it must return a `ProgressTracker` — a context manager with an
240+ ``update(increment: int)`` method that will be passed the size of each
241+ downloaded chunk as each chunk is received.
242+
228243 :param DistributionPackage pkg: the distribution package to download
229244 :param path:
230245 the path at which to save the downloaded file; any parent
@@ -234,6 +249,7 @@ def download_package(
234249 :param bool keep_on_error:
235250 whether to keep (true) or delete (false) the downloaded file if an
236251 error occurs
252+ :param progress: a callable for contructing a progress tracker
237253 :param timeout: optional timeout to pass to the ``requests`` call
238254 :type timeout: Union[float, Tuple[float,float], None]
239255 :raises requests.HTTPError: if the repository responds with an HTTP
@@ -255,10 +271,18 @@ def download_package(
255271 with self .s .get (pkg .url , stream = True , timeout = timeout ) as r :
256272 r .raise_for_status ()
257273 try :
258- with target .open ("wb" ) as fp :
259- for chunk in r .iter_content (65535 ):
260- fp .write (chunk )
261- digester .update (chunk )
274+ content_length = int (r .headers ["Content-Length" ])
275+ except (ValueError , KeyError ):
276+ content_length = None
277+ if progress is None :
278+ progress = null_progress_tracker ()
279+ try :
280+ with progress (content_length ) as p :
281+ with target .open ("wb" ) as fp :
282+ for chunk in r .iter_content (65535 ):
283+ fp .write (chunk )
284+ digester .update (chunk )
285+ p .update (len (chunk ))
262286 digester .finalize ()
263287 except Exception :
264288 if not keep_on_error :
0 commit comments