python-download 0.0.2__tar.gz → 0.0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_download-0.0.2 → python_download-0.0.2.2}/PKG-INFO +1 -1
- {python_download-0.0.2 → python_download-0.0.2.2}/download/__init__.py +273 -77
- {python_download-0.0.2 → python_download-0.0.2.2}/pyproject.toml +1 -1
- {python_download-0.0.2 → python_download-0.0.2.2}/LICENSE +0 -0
- {python_download-0.0.2 → python_download-0.0.2.2}/download/__main__.py +0 -0
- {python_download-0.0.2 → python_download-0.0.2.2}/download/py.typed +0 -0
- {python_download-0.0.2 → python_download-0.0.2.2}/readme.md +0 -0
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
# encoding: utf
|
|
3
3
|
|
|
4
4
|
__author__ = "ChenyangGao <https://chenyanggao.github.io>"
|
|
5
|
-
__version__ = (0, 0,
|
|
5
|
+
__version__ = (0, 0, 3)
|
|
6
6
|
__all__ = [
|
|
7
|
-
"
|
|
7
|
+
"DownloadTaskStatus", "DownloadProgress",
|
|
8
|
+
"DownloadTask", "AsyncDownloadTask",
|
|
9
|
+
#"DownloadTaskManager", "AsyncDownloadTaskManager",
|
|
8
10
|
"download_iter", "download", "download_async_iter", "download_async",
|
|
9
11
|
]
|
|
10
12
|
|
|
@@ -20,11 +22,16 @@ __all__ = [
|
|
|
20
22
|
|
|
21
23
|
import errno
|
|
22
24
|
|
|
23
|
-
from
|
|
25
|
+
from asyncio import create_task
|
|
26
|
+
from asyncio.exceptions import CancelledError, InvalidStateError
|
|
27
|
+
from abc import ABC, abstractmethod
|
|
28
|
+
from collections.abc import AsyncGenerator, AsyncIterator, Callable, Generator, Iterator
|
|
29
|
+
from enum import IntEnum
|
|
30
|
+
from inspect import isawaitable
|
|
24
31
|
from os import fsdecode, fstat, makedirs, PathLike
|
|
25
32
|
from os.path import abspath, dirname, isdir, join as joinpath
|
|
26
33
|
from shutil import COPY_BUFSIZE # type: ignore
|
|
27
|
-
from threading import Event
|
|
34
|
+
from threading import Event, Lock
|
|
28
35
|
from typing import cast, Any, NamedTuple, Self
|
|
29
36
|
|
|
30
37
|
from asynctools import ensure_aiter, ensure_async, as_thread
|
|
@@ -47,11 +54,31 @@ else:
|
|
|
47
54
|
setattr(FileIOWrapperBase, "__getattr__", lambda self, attr, /: getattr(self.file, attr))
|
|
48
55
|
|
|
49
56
|
|
|
57
|
+
class DownloadTaskStatus(IntEnum):
|
|
58
|
+
PENDING = 0
|
|
59
|
+
RUNNING = 1
|
|
60
|
+
PAUSED = 2
|
|
61
|
+
FINISHED = 3
|
|
62
|
+
CANCELED = 4
|
|
63
|
+
FAILED = 5
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def of(cls, val, /) -> Self:
|
|
67
|
+
if isinstance(val, cls):
|
|
68
|
+
return val
|
|
69
|
+
try:
|
|
70
|
+
if isinstance(val, str):
|
|
71
|
+
return cls[val]
|
|
72
|
+
except KeyError:
|
|
73
|
+
pass
|
|
74
|
+
return cls(val)
|
|
75
|
+
|
|
76
|
+
|
|
50
77
|
class DownloadProgress(NamedTuple):
|
|
51
78
|
total: int
|
|
52
79
|
downloaded: int
|
|
53
80
|
skipped: int
|
|
54
|
-
|
|
81
|
+
last_increment: int = 0
|
|
55
82
|
extra: Any = None
|
|
56
83
|
|
|
57
84
|
@property
|
|
@@ -71,23 +98,130 @@ class DownloadProgress(NamedTuple):
|
|
|
71
98
|
return self.completed >= self.total
|
|
72
99
|
|
|
73
100
|
|
|
74
|
-
class
|
|
101
|
+
class BaseDownloadTask(ABC):
|
|
102
|
+
state: DownloadTaskStatus = DownloadTaskStatus.PENDING
|
|
103
|
+
progress: None | DownloadProgress = None
|
|
104
|
+
_exception: None | BaseException
|
|
75
105
|
|
|
76
|
-
def __init__(self,
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
self.
|
|
81
|
-
self._gen = gen
|
|
82
|
-
self._done_event = Event()
|
|
106
|
+
def __init__(self, /):
|
|
107
|
+
self.done_callbacks: list[Callable[[Self], Any]] = []
|
|
108
|
+
|
|
109
|
+
def __del__(self, /):
|
|
110
|
+
self.cancel()
|
|
83
111
|
|
|
84
112
|
def __repr__(self, /) -> str:
|
|
113
|
+
name = type(self).__qualname__
|
|
85
114
|
match state := self.state:
|
|
86
|
-
case
|
|
87
|
-
return f"<{
|
|
88
|
-
case
|
|
89
|
-
return f"<{
|
|
90
|
-
return f"<{
|
|
115
|
+
case DownloadTaskStatus.FINISHED:
|
|
116
|
+
return f"<{name} :: state={state!r} result={self.result()} progress={self.progress!r}>"
|
|
117
|
+
case DownloadTaskStatus.FAILED:
|
|
118
|
+
return f"<{name} :: state={state!r} exception={self.exception()} progress={self.progress!r}>"
|
|
119
|
+
return f"<{name} :: state={state!r} progress={self.progress!r}>"
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def pending(self, /) -> bool:
|
|
123
|
+
return self.state is DownloadTaskStatus.PENDING
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def running(self, /) -> bool:
|
|
127
|
+
return self.state is DownloadTaskStatus.RUNNING
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def paused(self, /) -> bool:
|
|
131
|
+
return self.state is DownloadTaskStatus.PAUSED
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def finished(self, /) -> bool:
|
|
135
|
+
return self.state is DownloadTaskStatus.FINISHED
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def canceled(self, /) -> bool:
|
|
139
|
+
return self.state is DownloadTaskStatus.CANCELED
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def failed(self, /) -> bool:
|
|
143
|
+
return self.state is DownloadTaskStatus.FAILED
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def processing(self, /) -> bool:
|
|
147
|
+
return self.state in (DownloadTaskStatus.RUNNING, DownloadTaskStatus.PAUSED)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def done(self, /) -> bool:
|
|
151
|
+
return self.state is (DownloadTaskStatus.FINISHED, DownloadTaskStatus.CANCELED, DownloadTaskStatus.FAILED)
|
|
152
|
+
|
|
153
|
+
def cancel(self, /):
|
|
154
|
+
if not self.done:
|
|
155
|
+
self.set_exception(CancelledError())
|
|
156
|
+
self.state = DownloadTaskStatus.CANCELED
|
|
157
|
+
|
|
158
|
+
def pause(self, /):
|
|
159
|
+
if self.processing:
|
|
160
|
+
self.state = DownloadTaskStatus.PAUSED
|
|
161
|
+
else:
|
|
162
|
+
raise InvalidStateError(f"can't pause when state={self.state!r}")
|
|
163
|
+
|
|
164
|
+
def exception(self, /) -> None | BaseException:
|
|
165
|
+
if self.done:
|
|
166
|
+
return self._exception
|
|
167
|
+
else:
|
|
168
|
+
raise InvalidStateError(self.state)
|
|
169
|
+
|
|
170
|
+
def set_exception(self, exception, /):
|
|
171
|
+
self._exception = exception
|
|
172
|
+
|
|
173
|
+
def result(self, /):
|
|
174
|
+
if self.finished:
|
|
175
|
+
return self._result
|
|
176
|
+
elif not self.done:
|
|
177
|
+
raise InvalidStateError(self.state)
|
|
178
|
+
else:
|
|
179
|
+
raise cast(BaseException, self._exception)
|
|
180
|
+
|
|
181
|
+
def set_result(self, result, /):
|
|
182
|
+
self._result = result
|
|
183
|
+
|
|
184
|
+
def add_done_callback(self, /, callback: Callable[[Self], Any]):
|
|
185
|
+
if self.done:
|
|
186
|
+
callback(self)
|
|
187
|
+
else:
|
|
188
|
+
self.done_callbacks.append(callback)
|
|
189
|
+
|
|
190
|
+
def remove_done_callback(self, /, callback: int | slice | Callable[[Self], Any] = -1):
|
|
191
|
+
try:
|
|
192
|
+
if callable(callback):
|
|
193
|
+
self.done_callbacks.remove(callback)
|
|
194
|
+
else:
|
|
195
|
+
del self.done_callbacks[callback]
|
|
196
|
+
except (IndexError, ValueError):
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
@abstractmethod
|
|
200
|
+
def run(self, /):
|
|
201
|
+
match state := self.state:
|
|
202
|
+
case DownloadTaskStatus.PENDING | DownloadTaskStatus.PAUSED:
|
|
203
|
+
self.state = DownloadTaskStatus.RUNNING
|
|
204
|
+
case DownloadTaskStatus.RUNNING:
|
|
205
|
+
raise RuntimeError("already running")
|
|
206
|
+
case _:
|
|
207
|
+
raise RuntimeError(f"can't run when state={state!r}")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class DownloadTask(BaseDownloadTask):
|
|
211
|
+
|
|
212
|
+
def __init__(
|
|
213
|
+
self,
|
|
214
|
+
it: Iterator[DownloadProgress],
|
|
215
|
+
/,
|
|
216
|
+
submit=run_as_thread,
|
|
217
|
+
):
|
|
218
|
+
super().__init__()
|
|
219
|
+
if not callable(submit):
|
|
220
|
+
submit = submit.submit
|
|
221
|
+
self.submit = submit
|
|
222
|
+
self._it = it
|
|
223
|
+
self._state_lock = Lock()
|
|
224
|
+
self._done_event = Event()
|
|
91
225
|
|
|
92
226
|
@classmethod
|
|
93
227
|
def create_task(
|
|
@@ -99,82 +233,144 @@ class DownloadTask:
|
|
|
99
233
|
) -> Self:
|
|
100
234
|
return cls(download_iter(*args, **kwargs), submit=submit)
|
|
101
235
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
236
|
+
def add_done_callback(self, /, callback: Callable[[Self], Any]):
|
|
237
|
+
with self._state_lock:
|
|
238
|
+
if not self.done:
|
|
239
|
+
self.done_callbacks.append(callback)
|
|
240
|
+
return
|
|
241
|
+
return callback(self)
|
|
105
242
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
243
|
+
def cancel(self, /):
|
|
244
|
+
with self._state_lock:
|
|
245
|
+
super().cancel()
|
|
246
|
+
self._done_event.set()
|
|
109
247
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
248
|
+
def pause(self, /):
|
|
249
|
+
with self._state_lock:
|
|
250
|
+
super().pause()
|
|
251
|
+
|
|
252
|
+
def exception(self, /, timeout: None | float = None) -> None | BaseException:
|
|
253
|
+
self._done_event.wait(timeout)
|
|
254
|
+
return super().exception()
|
|
114
255
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
self._result = val
|
|
256
|
+
def set_exception(self, exception, /):
|
|
257
|
+
super().set_exception(exception)
|
|
118
258
|
self._done_event.set()
|
|
119
259
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return
|
|
260
|
+
def result(self, /, timeout: None | float = None):
|
|
261
|
+
self._done_event.wait(timeout)
|
|
262
|
+
return super().result()
|
|
123
263
|
|
|
124
|
-
def
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
else:
|
|
128
|
-
state = self._state
|
|
129
|
-
self._state = "CANCELED"
|
|
130
|
-
if state != "RUNNING":
|
|
131
|
-
self.run()
|
|
264
|
+
def set_result(self, result, /):
|
|
265
|
+
super().set_result(result)
|
|
266
|
+
self._done_event.set()
|
|
132
267
|
|
|
133
|
-
def
|
|
134
|
-
|
|
135
|
-
|
|
268
|
+
def run(self, /):
|
|
269
|
+
super().run()
|
|
270
|
+
state_lock = self._state_lock
|
|
271
|
+
it = self._it
|
|
272
|
+
step = it.__next__
|
|
273
|
+
try:
|
|
274
|
+
while self.running:
|
|
275
|
+
self.progress = step()
|
|
276
|
+
except KeyboardInterrupt:
|
|
277
|
+
raise
|
|
278
|
+
except StopIteration as exc:
|
|
279
|
+
with state_lock:
|
|
280
|
+
self.state = DownloadTaskStatus.FINISHED
|
|
281
|
+
self.set_result(exc.value)
|
|
282
|
+
except BaseException as exc:
|
|
283
|
+
with state_lock:
|
|
284
|
+
self.state = DownloadTaskStatus.FAILED
|
|
285
|
+
self.set_exception(exc)
|
|
136
286
|
else:
|
|
137
|
-
|
|
287
|
+
if self.done:
|
|
288
|
+
try:
|
|
289
|
+
getattr(it, "__del__")()
|
|
290
|
+
except:
|
|
291
|
+
pass
|
|
292
|
+
for callback in self.done_callbacks:
|
|
293
|
+
try:
|
|
294
|
+
callback(cast(Self, self))
|
|
295
|
+
except:
|
|
296
|
+
pass
|
|
297
|
+
|
|
298
|
+
def start(self, /, wait: bool = False):
|
|
299
|
+
with self._state_lock:
|
|
300
|
+
if self.state in (DownloadTaskStatus.PENDING, DownloadTaskStatus.PAUSED):
|
|
301
|
+
self.submit(self.run)
|
|
302
|
+
if wait and not self.done:
|
|
303
|
+
self._done_event.wait()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class AsyncDownloadTask(BaseDownloadTask):
|
|
307
|
+
|
|
308
|
+
def __init__(
|
|
309
|
+
self,
|
|
310
|
+
it: AsyncIterator[DownloadProgress],
|
|
311
|
+
/,
|
|
312
|
+
submit=create_task,
|
|
313
|
+
):
|
|
314
|
+
super().__init__()
|
|
315
|
+
if not callable(submit):
|
|
316
|
+
submit = submit.submit
|
|
317
|
+
self.submit = submit
|
|
318
|
+
self._it = it
|
|
138
319
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
320
|
+
@classmethod
|
|
321
|
+
def create_task(
|
|
322
|
+
cls,
|
|
323
|
+
/,
|
|
324
|
+
*args,
|
|
325
|
+
submit=create_task,
|
|
326
|
+
**kwargs,
|
|
327
|
+
) -> Self:
|
|
328
|
+
return cls(download_async_iter(*args, **kwargs), submit=submit)
|
|
329
|
+
|
|
330
|
+
async def run(self, /):
|
|
331
|
+
super().run()
|
|
332
|
+
it = self._it
|
|
333
|
+
step = it.__anext__
|
|
145
334
|
try:
|
|
146
|
-
while self.
|
|
147
|
-
self.
|
|
335
|
+
while self.running:
|
|
336
|
+
self.progress = await step()
|
|
148
337
|
except KeyboardInterrupt:
|
|
149
338
|
raise
|
|
150
339
|
except StopIteration as exc:
|
|
151
|
-
self.
|
|
152
|
-
self.
|
|
340
|
+
self.state = DownloadTaskStatus.FINISHED
|
|
341
|
+
self.set_result(exc.value)
|
|
153
342
|
except BaseException as exc:
|
|
154
|
-
self.
|
|
155
|
-
self.
|
|
343
|
+
self.state = DownloadTaskStatus.FAILED
|
|
344
|
+
self.set_exception(exc)
|
|
156
345
|
else:
|
|
157
|
-
if self.
|
|
346
|
+
if self.canceled:
|
|
158
347
|
try:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
348
|
+
if isinstance(it, AsyncGenerator):
|
|
349
|
+
await it.aclose()
|
|
350
|
+
else:
|
|
351
|
+
getattr(it, "__del__")()
|
|
352
|
+
except:
|
|
353
|
+
pass
|
|
354
|
+
elif self.done:
|
|
355
|
+
for callback in self.done_callbacks:
|
|
356
|
+
try:
|
|
357
|
+
ret = callback(cast(Self, self))
|
|
358
|
+
if isawaitable(ret):
|
|
359
|
+
await ret
|
|
360
|
+
except:
|
|
361
|
+
pass
|
|
162
362
|
|
|
163
|
-
def
|
|
164
|
-
|
|
363
|
+
def start(self, /):
|
|
364
|
+
if self.state in (DownloadTaskStatus.PENDING, DownloadTaskStatus.PAUSED):
|
|
365
|
+
return self.submit(self.run())
|
|
165
366
|
|
|
166
|
-
def run_wait(self, /):
|
|
167
|
-
if not self._done_event.is_set():
|
|
168
|
-
if self._state == "RUNNING":
|
|
169
|
-
self._done_event.wait()
|
|
170
|
-
else:
|
|
171
|
-
self._run()
|
|
172
367
|
|
|
368
|
+
class DownloadTaskManager:
|
|
369
|
+
...
|
|
173
370
|
|
|
174
|
-
class AsyncDownloadTask:
|
|
175
371
|
|
|
176
|
-
|
|
177
|
-
|
|
372
|
+
class AsyncDownloadTaskManager:
|
|
373
|
+
...
|
|
178
374
|
|
|
179
375
|
|
|
180
376
|
def download_iter(
|
|
@@ -307,13 +503,13 @@ def download(
|
|
|
307
503
|
else:
|
|
308
504
|
update = reporthook
|
|
309
505
|
close = getattr(reporthook, "close", None)
|
|
310
|
-
update(progress.
|
|
506
|
+
update(progress.last_increment)
|
|
311
507
|
if update is None:
|
|
312
508
|
for progress in download_gen:
|
|
313
509
|
pass
|
|
314
510
|
else:
|
|
315
511
|
for progress in download_gen:
|
|
316
|
-
update(progress.
|
|
512
|
+
update(progress.last_increment)
|
|
317
513
|
return progress
|
|
318
514
|
finally:
|
|
319
515
|
download_gen.close()
|
|
@@ -468,13 +664,13 @@ async def download_async(
|
|
|
468
664
|
else:
|
|
469
665
|
update = ensure_async(reporthook)
|
|
470
666
|
close = getattr(reporthook, "close", None)
|
|
471
|
-
await update(progress.
|
|
667
|
+
await update(progress.last_increment)
|
|
472
668
|
if update is None:
|
|
473
669
|
async for progress in download_gen:
|
|
474
670
|
pass
|
|
475
671
|
else:
|
|
476
672
|
async for progress in download_gen:
|
|
477
|
-
await update(progress.
|
|
673
|
+
await update(progress.last_increment)
|
|
478
674
|
finally:
|
|
479
675
|
await download_gen.aclose()
|
|
480
676
|
if close is not None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|