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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-download
3
- Version: 0.0.2
3
+ Version: 0.0.2.2
4
4
  Summary: Python for download.
5
5
  Home-page: https://github.com/ChenyangGao/web-mount-packs/tree/main/python-module/python-download
6
6
  License: MIT
@@ -2,9 +2,11 @@
2
2
  # encoding: utf
3
3
 
4
4
  __author__ = "ChenyangGao <https://chenyanggao.github.io>"
5
- __version__ = (0, 0, 2)
5
+ __version__ = (0, 0, 3)
6
6
  __all__ = [
7
- "DownloadProgress", "DownloadTask", "AsyncDownloadTask",
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 collections.abc import AsyncGenerator, Generator, Callable, Generator
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
- last_incr: int = 0
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 DownloadTask:
101
+ class BaseDownloadTask(ABC):
102
+ state: DownloadTaskStatus = DownloadTaskStatus.PENDING
103
+ progress: None | DownloadProgress = None
104
+ _exception: None | BaseException
75
105
 
76
- def __init__(self, /, gen, submit=run_as_thread):
77
- if not callable(submit):
78
- submit = submit.submit
79
- self._submit = submit
80
- self._state = "PENDING"
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 "FINISHED":
87
- return f"<{type(self).__qualname__} :: state={state!r} result={self.result} progress={self.progress!r}>"
88
- case "FAILED":
89
- return f"<{type(self).__qualname__} :: state={state!r} reason={self.result} progress={self.progress!r}>"
90
- return f"<{type(self).__qualname__} :: state={state!r} progress={self.progress!r}>"
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
- @property
103
- def closed(self, /) -> bool:
104
- return self._state in ("CANCELED", "FAILED", "FINISHED")
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
- @property
107
- def progress(self, /) -> None | DownloadProgress:
108
- return self.__dict__.get("_progress")
243
+ def cancel(self, /):
244
+ with self._state_lock:
245
+ super().cancel()
246
+ self._done_event.set()
109
247
 
110
- @property
111
- def result(self, /):
112
- self._done_event.wait()
113
- return self._result
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
- @result.setter
116
- def result(self, val, /):
117
- self._result = val
256
+ def set_exception(self, exception, /):
257
+ super().set_exception(exception)
118
258
  self._done_event.set()
119
259
 
120
- @property
121
- def state(self, /) -> str:
122
- return self._state
260
+ def result(self, /, timeout: None | float = None):
261
+ self._done_event.wait(timeout)
262
+ return super().result()
123
263
 
124
- def close(self, /):
125
- if self._state in ("CANCELED", "FAILED", "FINISHED"):
126
- pass
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 pause(self, /):
134
- if self._state in ("PAUSED", "RUNNING"):
135
- self._state = "PAUSED"
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
- raise RuntimeError(f"can't pause when state={self._state!r}")
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
- def _run(self, /):
140
- if self._state in ("PENDING", "PAUSED"):
141
- self._state = "RUNNING"
142
- else:
143
- raise RuntimeError(f"can't run when state={self._state!r}")
144
- gen = self._gen
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._state == "RUNNING":
147
- self._progress = next(gen)
335
+ while self.running:
336
+ self.progress = await step()
148
337
  except KeyboardInterrupt:
149
338
  raise
150
339
  except StopIteration as exc:
151
- self._state = "FINISHED"
152
- self.result = exc.value
340
+ self.state = DownloadTaskStatus.FINISHED
341
+ self.set_result(exc.value)
153
342
  except BaseException as exc:
154
- self._state = "FAILED"
155
- self.result = exc
343
+ self.state = DownloadTaskStatus.FAILED
344
+ self.set_exception(exc)
156
345
  else:
157
- if self._state == "CANCELED":
346
+ if self.canceled:
158
347
  try:
159
- gen.close()
160
- finally:
161
- self.result = None
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 run(self, /):
164
- return self._submit(self._run)
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
- def __init__(self, /, *args, **kwargs):
177
- raise NotImplementedError
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.last_incr)
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.last_incr)
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.last_incr)
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.last_incr)
673
+ await update(progress.last_increment)
478
674
  finally:
479
675
  await download_gen.aclose()
480
676
  if close is not None:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "python-download"
3
- version = "0.0.2"
3
+ version = "0.0.2.2"
4
4
  description = "Python for download."
5
5
  authors = ["ChenyangGao <wosiwujm@gmail.com>"]
6
6
  license = "MIT"