1
1
import asyncio
2
2
import logging
3
+ from functools import wraps
3
4
from os import PathLike
4
5
from pathlib import Path
5
6
from typing import Awaitable , Callable , Optional
13
14
__all__ = ["download" ]
14
15
15
16
17
+ def _guard (func : Callable ):
18
+ warned = False
19
+
20
+ @wraps (func )
21
+ async def wrapper (* args , ** kwds ):
22
+ try :
23
+ return await func (* args , ** kwds )
24
+ except :
25
+ nonlocal warned
26
+ if not warned :
27
+ log .error ("Error caught by guard." , exc_info = True )
28
+ warned = True
29
+
30
+ return wrapper
31
+
32
+
16
33
async def download (
17
34
url : URLTypes ,
18
35
local : Optional [PathLike ] = None ,
19
36
buffer : int = 32768 ,
20
37
echo_progress : Optional [Callable [..., Awaitable ]] = None ,
21
- proxy : ProxiesTypes = ...,
22
- ):
38
+ * ,
39
+ client : Optional [AsyncClient ] = None ,
40
+ proxy : Optional [ProxiesTypes ] = None ,
41
+ ) -> int :
23
42
"""async-download a url to a the given path.
24
43
25
44
:param local: where to download
45
+ :param buffer: transfer buffer
26
46
:param echo_progress: async function to receive downloaded size and total size as int.
27
-
47
+ :param client: use this client, otherwise we will create one and close it on return.
48
+ :param proxy: download proxy.
28
49
:return: size
50
+
51
+ .. warning:: actual proxy = client.proxy | param.proxy
52
+
53
+ .. versionchanged:: 1.4.2
54
+
55
+ added `client`; `proxy` is set as a keyword-only parameter.
29
56
"""
57
+ if client is None :
58
+ async with AsyncClient (proxies = proxy ) as client :
59
+ return await download (url , local , buffer , echo_progress , client = client , proxy = proxy )
60
+
30
61
url = url if isinstance (url , URL ) else URL (url )
31
62
remote_name = Path (url .path ).name
32
63
local = Path (local or remote_name )
33
64
local .parent .mkdir (parents = True , exist_ok = True )
34
65
35
66
if local .is_dir ():
67
+ # if local not exist or is a directory
36
68
local = local / remote_name
37
69
log .info (f"Changing local path from { local .parent .as_posix ()} to { local .as_posix ()} " )
38
70
if local .is_dir ():
@@ -41,29 +73,29 @@ async def download(
41
73
if local .exists ():
42
74
log .warning (f"{ local .as_posix ()} exists. Overwrite." )
43
75
44
- client_dict = {}
45
- if proxy != ...:
46
- client_dict ["proxies" ] = proxy
76
+ if echo_progress :
77
+ echo_progress = _guard (echo_progress )
47
78
48
79
acc = 0
49
- async with AsyncClient (** client_dict ) as client , aiofiles .open (local , "wb" ) as f :
50
- r = await client .get (url , follow_redirects = True )
51
- r .raise_for_status ()
52
-
53
- size = int (r .headers .get ("Content-Length" , - 1 ))
54
- it = r .aiter_bytes (chunk_size = buffer )
55
-
56
- log .info (f"Starting download task { url } -> { local } " )
57
- log .debug (f"File size: { size } " )
58
-
59
- async for c in it :
60
- if not c :
61
- continue
62
- if echo_progress :
63
- ad , _ = await asyncio .gather (f .write (c ), echo_progress (completed = acc , total = size ))
64
- acc += ad
65
- else :
66
- acc += await f .write (c )
80
+ async with aiofiles .open (local , "wb" ) as f :
81
+ async with client .stream ("GET" , url , follow_redirects = True ) as r :
82
+ r .raise_for_status ()
83
+
84
+ size = int (r .headers .get ("Content-Length" , - 1 ))
85
+
86
+ log .info (f"Starting download task { url } -> { local } " )
87
+ log .debug (f"File size: { size } " )
88
+
89
+ async for c in r .aiter_bytes (chunk_size = buffer ):
90
+ if not c :
91
+ continue
92
+ if echo_progress :
93
+ ad , _ = await asyncio .gather (
94
+ f .write (c ), echo_progress (completed = acc , total = size )
95
+ )
96
+ acc += ad
97
+ else :
98
+ acc += await f .write (c )
67
99
68
100
if size > 0 and acc != size :
69
101
log .error (f"Content-Length is { size } but { acc } downloaded" )
0 commit comments