63 lines
1.7 KiB
Python
63 lines
1.7 KiB
Python
import dataclasses
|
|
import functools
|
|
import io
|
|
import pathlib
|
|
from datetime import timedelta, datetime
|
|
from io import FileIO
|
|
from typing import Callable
|
|
|
|
import httpx
|
|
|
|
|
|
class ProgressIO(io.BufferedReader):
|
|
@dataclasses.dataclass
|
|
class Progress:
|
|
total: int
|
|
processed: int
|
|
|
|
def percent(self) -> float:
|
|
return round(100 * self.processed / self.total, 2)
|
|
|
|
def __init__(self, file: pathlib.Path, on_progress: Callable[[Progress], None]):
|
|
super().__init__(raw=FileIO(str(file)))
|
|
self._size = file.stat().st_size
|
|
self._on_progress_throttled = self.throttle(1)(on_progress)
|
|
self._on_progress = on_progress
|
|
self._processed = 0
|
|
self._iter = None
|
|
|
|
def __next__(self) -> bytes:
|
|
chunk = super().__next__()
|
|
self._processed += len(chunk)
|
|
progress = ProgressIO.Progress(self._size, self._processed)
|
|
if self._processed == self._size:
|
|
self._on_progress(progress)
|
|
else:
|
|
self._on_progress_throttled(progress)
|
|
|
|
return chunk
|
|
|
|
@staticmethod
|
|
def throttle(seconds: float = 0):
|
|
period = timedelta(seconds=seconds)
|
|
|
|
def decorator(fn):
|
|
last_called = datetime.min
|
|
|
|
@functools.wraps(fn)
|
|
def wrapper(*args, **kwargs):
|
|
now = datetime.now()
|
|
nonlocal last_called
|
|
if now - last_called > period:
|
|
last_called = now
|
|
return fn(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
return decorator
|
|
|
|
|
|
if __name__ == "__main__":
|
|
f = pathlib.Path(r"/path/to/file")
|
|
httpx.post("http://httpbin.org/post", files={"file": ProgressIO(f, on_progress=lambda p: print(p.percent()))})
|