9
9
from contextlib import suppress
10
10
from email .message import Message
11
11
from pathlib import Path
12
- from urllib .parse import urlparse
13
12
14
- import requests .exceptions as requests_exceptions
15
- from requests import Response
13
+ import httpx
16
14
17
15
from briefcase .exceptions import (
18
16
BadNetworkResourceError ,
@@ -176,57 +174,76 @@ def download(self, url: str, download_path: Path, role: str | None = None) -> Pa
176
174
download_path .mkdir (parents = True , exist_ok = True )
177
175
filename : Path = None
178
176
try :
179
- response = self .tools .requests .get (url , stream = True )
180
- if response .status_code == 404 :
181
- raise MissingNetworkResourceError (url = url )
182
- elif response .status_code != 200 :
183
- raise BadNetworkResourceError (url = url , status_code = response .status_code )
184
-
185
- # The initial URL might (read: will) go through URL redirects, so
186
- # we need the *final* response. We look at either the `Content-Disposition`
187
- # header, or the final URL, to extract the cache filename.
188
- cache_full_name = urlparse (response .url ).path
189
- header_value = response .headers .get ("Content-Disposition" )
190
- if header_value :
191
- # Neither requests nor httplib provides a way to parse RFC6266 headers.
192
- # The cgi module *did* have a way to parse these headers, but
193
- # it was deprecated as part of PEP594. PEP594 recommends
194
- # using the email.message module to parse these headers as they
195
- # are near identical format.
196
- # See also:
197
- # * https://tools.ietf.org/html/rfc6266
198
- # * https://peps.python.org/pep-0594/#cgi
199
- msg = Message ()
200
- msg ["Content-Disposition" ] = header_value
201
- filename = msg .get_filename ()
202
- if filename :
203
- cache_full_name = filename
204
- cache_name = cache_full_name .split ("/" )[- 1 ]
205
- filename = download_path / cache_name
206
-
207
- if filename .exists ():
208
- self .tools .logger .info (f"{ cache_name } already downloaded" )
209
- else :
210
- self .tools .logger .info (f"Downloading { cache_name } ..." )
211
- self ._fetch_and_write_content (response , filename )
212
- except requests_exceptions .ConnectionError as e :
177
+ with self .tools .httpx .stream ("GET" , url , follow_redirects = True ) as response :
178
+ if response .status_code == 404 :
179
+ raise MissingNetworkResourceError (url = url )
180
+ elif response .status_code != 200 :
181
+ raise BadNetworkResourceError (
182
+ url = url , status_code = response .status_code
183
+ )
184
+
185
+ # The initial URL might (read: will) go through URL redirects, so
186
+ # we need the *final* response. We look at either the `Content-Disposition`
187
+ # header, or the final URL, to extract the cache filename.
188
+ cache_full_name = response .url .path
189
+ header_value = response .headers .get ("Content-Disposition" )
190
+ if header_value :
191
+ # Httpx does not provide a way to parse RFC6266 headers.
192
+ # The cgi module *did* have a way to parse these headers, but
193
+ # it was deprecated as part of PEP594. PEP594 recommends
194
+ # using the email.message module to parse these headers as they
195
+ # are near identical format.
196
+ # See also:
197
+ # * https://tools.ietf.org/html/rfc6266
198
+ # * https://peps.python.org/pep-0594/#cgi
199
+ msg = Message ()
200
+ msg ["Content-Disposition" ] = header_value
201
+ filename = msg .get_filename ()
202
+ if filename :
203
+ cache_full_name = filename
204
+ cache_name = cache_full_name .split ("/" )[- 1 ]
205
+ filename = download_path / cache_name
206
+
207
+ if filename .exists ():
208
+ self .tools .logger .info (f"{ cache_name } already downloaded" )
209
+ else :
210
+ self .tools .logger .info (f"Downloading { cache_name } ..." )
211
+ self ._fetch_and_write_content (response , filename )
212
+ except httpx .RequestError as e :
213
213
if role :
214
214
description = role
215
215
else :
216
216
description = filename .name if filename else url
217
- raise NetworkFailure (f"download { description } " ) from e
217
+
218
+ if isinstance (e , httpx .TooManyRedirects ):
219
+ # httpx, unlike requests, will not follow redirects indefinitely, and defaults to
220
+ # 20 redirects before calling it quits. If the download attempt exceeds 20 redirects,
221
+ # Briefcase probably needs to re-evaluate the URLs it is using for that download
222
+ # and ideally find a starting point that won't have so many redirects.
223
+ hint = "exceeded redirects when downloading the file.\n \n Please report this as a bug to Briefcase."
224
+ elif isinstance (e , httpx .DecodingError ):
225
+ hint = "the server sent a malformed response."
226
+ else :
227
+ # httpx.TransportError
228
+ # Use the default hint for generic network communication errors
229
+ hint = None
230
+
231
+ raise NetworkFailure (
232
+ f"download { description } " ,
233
+ hint ,
234
+ ) from e
218
235
219
236
return filename
220
237
221
- def _fetch_and_write_content (self , response : Response , filename : Path ):
222
- """Write the content from the requests Response to file.
238
+ def _fetch_and_write_content (self , response : httpx . Response , filename : Path ):
239
+ """Write the content from the httpx Response to file.
223
240
224
241
The data is initially written in to a temporary file in the Briefcase
225
242
cache. This avoids partially downloaded files masquerading as complete
226
243
downloads in later Briefcase runs. The temporary file is only moved
227
244
to ``filename`` if the download is successful; otherwise, it is deleted.
228
245
229
- :param response: ``requests .Response``
246
+ :param response: ``httpx .Response``
230
247
:param filename: full filesystem path to save data
231
248
"""
232
249
temp_file = tempfile .NamedTemporaryFile (
@@ -239,12 +256,13 @@ def _fetch_and_write_content(self, response: Response, filename: Path):
239
256
with temp_file :
240
257
total = response .headers .get ("content-length" )
241
258
if total is None :
259
+ response .read ()
242
260
temp_file .write (response .content )
243
261
else :
244
262
progress_bar = self .tools .input .progress_bar ()
245
263
task_id = progress_bar .add_task ("Downloader" , total = int (total ))
246
264
with progress_bar :
247
- for data in response .iter_content (chunk_size = 1024 * 1024 ):
265
+ for data in response .iter_bytes (chunk_size = 1024 * 1024 ):
248
266
temp_file .write (data )
249
267
progress_bar .update (task_id , advance = len (data ))
250
268
0 commit comments