big upgrade UwU
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
"""Console reporting for a download run."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import IO, Protocol
|
||||
|
||||
from sticker_downloader.results import (
|
||||
FileOutcome,
|
||||
FileStatus,
|
||||
PackResult,
|
||||
RunTotals,
|
||||
format_size,
|
||||
)
|
||||
|
||||
|
||||
class ProgressReporter(Protocol):
|
||||
"""Everything the downloader and CLI report as a run unfolds."""
|
||||
|
||||
def info(self, message: str) -> None: ...
|
||||
def warn(self, message: str) -> None: ...
|
||||
def error(self, message: str) -> None: ...
|
||||
def pack_started(
|
||||
self, reference: str, name: str, total_stickers: int, total_files: int
|
||||
) -> None: ...
|
||||
def file_finished(self, outcome: FileOutcome) -> None: ...
|
||||
def pack_finished(self, result: PackResult) -> None: ...
|
||||
def run_finished(self, results: list[PackResult]) -> None: ...
|
||||
|
||||
|
||||
class NullProgress:
|
||||
"""Reports nothing. Used by ``--quiet`` and by tests."""
|
||||
|
||||
def info(self, message: str) -> None: ...
|
||||
def warn(self, message: str) -> None: ...
|
||||
def error(self, message: str) -> None: ...
|
||||
def pack_started(
|
||||
self, reference: str, name: str, total_stickers: int, total_files: int
|
||||
) -> None: ...
|
||||
def file_finished(self, outcome: FileOutcome) -> None: ...
|
||||
def pack_finished(self, result: PackResult) -> None: ...
|
||||
def run_finished(self, results: list[PackResult]) -> None: ...
|
||||
|
||||
|
||||
class ConsoleProgress:
|
||||
"""Writes human-friendly progress to a stream.
|
||||
|
||||
On a terminal the per-pack counters are redrawn in place; when the output is
|
||||
piped to a file each pack reports a single summary line instead, so logs stay
|
||||
readable.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
stream: IO[str] | None = None,
|
||||
*,
|
||||
verbose: bool = False,
|
||||
use_ansi: bool | None = None,
|
||||
) -> None:
|
||||
self._stream = stream if stream is not None else sys.stderr
|
||||
self._verbose = verbose
|
||||
if use_ansi is None:
|
||||
use_ansi = bool(getattr(self._stream, "isatty", lambda: False)())
|
||||
self._use_ansi = use_ansi
|
||||
self._live_width = 0
|
||||
self._pack_total = 0
|
||||
self._pack_done = 0
|
||||
|
||||
# -- plain messages ---------------------------------------------------
|
||||
|
||||
def info(self, message: str) -> None:
|
||||
self._write_line(message)
|
||||
|
||||
def warn(self, message: str) -> None:
|
||||
self._write_line(f"warning: {message}")
|
||||
|
||||
def error(self, message: str) -> None:
|
||||
self._write_line(f"error: {message}")
|
||||
|
||||
# -- run lifecycle ----------------------------------------------------
|
||||
|
||||
def pack_started(
|
||||
self, reference: str, name: str, total_stickers: int, total_files: int
|
||||
) -> None:
|
||||
self._pack_total = total_files
|
||||
self._pack_done = 0
|
||||
self._write_line(f"\n{name}: {total_stickers} stickers, {total_files} files")
|
||||
|
||||
def file_finished(self, outcome: FileOutcome) -> None:
|
||||
self._pack_done += 1
|
||||
if outcome.status is FileStatus.FAILED:
|
||||
self.error(f"{outcome.name}: {outcome.error}")
|
||||
return
|
||||
if self._verbose:
|
||||
detail = (
|
||||
f" ({format_size(outcome.size)})"
|
||||
if outcome.status is FileStatus.DOWNLOADED
|
||||
else ""
|
||||
)
|
||||
self._write_line(f" {outcome.status.value}: {outcome.name}{detail}")
|
||||
return
|
||||
total = self._pack_total or self._pack_done
|
||||
self._draw_live(f" {self._pack_done}/{total} files")
|
||||
|
||||
def pack_finished(self, result: PackResult) -> None:
|
||||
self._clear_live()
|
||||
if result.error is not None:
|
||||
self.error(f"{result.reference}: {result.error}")
|
||||
return
|
||||
location = f" -> {result.directory}" if result.directory else ""
|
||||
self._write_line(f" {result.summary()}{location}")
|
||||
|
||||
def run_finished(self, results: list[PackResult]) -> None:
|
||||
self._clear_live()
|
||||
totals = RunTotals.from_results(results)
|
||||
if totals.packs == 0:
|
||||
self._write_line("\nNothing to do.")
|
||||
return
|
||||
written = format_size(totals.bytes_written)
|
||||
lines = [
|
||||
"",
|
||||
f"Done: {totals.packs_ok}/{totals.packs} packs",
|
||||
f" files downloaded : {totals.downloaded} ({written})",
|
||||
]
|
||||
if totals.planned:
|
||||
lines.append(f" files planned : {totals.planned}")
|
||||
if totals.skipped:
|
||||
lines.append(f" already present : {totals.skipped}")
|
||||
if totals.failed:
|
||||
lines.append(f" files failed : {totals.failed}")
|
||||
if totals.packs_failed:
|
||||
lines.append(f" packs failed : {totals.packs_failed}")
|
||||
for result in results:
|
||||
if not result.ok:
|
||||
reason = result.error or f"{result.failed} file(s) failed"
|
||||
lines.append(f" - {result.reference}: {reason}")
|
||||
self._write_line("\n".join(lines))
|
||||
|
||||
# -- internals --------------------------------------------------------
|
||||
|
||||
def _draw_live(self, text: str) -> None:
|
||||
if not self._use_ansi:
|
||||
return
|
||||
padding = " " * max(0, self._live_width - len(text))
|
||||
self._stream.write(f"\r{text}{padding}")
|
||||
self._stream.flush()
|
||||
self._live_width = len(text)
|
||||
|
||||
def _clear_live(self) -> None:
|
||||
if self._use_ansi and self._live_width:
|
||||
self._stream.write("\r" + " " * self._live_width + "\r")
|
||||
self._stream.flush()
|
||||
self._live_width = 0
|
||||
|
||||
def _write_line(self, text: str) -> None:
|
||||
self._clear_live()
|
||||
self._stream.write(f"{text}\n")
|
||||
self._stream.flush()
|
||||
Reference in New Issue
Block a user