mqdm 2.0.0__py3-none-any.whl
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.
- mqdm/__init__.py +96 -0
- mqdm/backend/__init__.py +24 -0
- mqdm/backend/protocols.py +71 -0
- mqdm/backend/rich.py +302 -0
- mqdm/backend/rich_progress_columns.py +158 -0
- mqdm/bar.py +564 -0
- mqdm/events/__init__.py +28 -0
- mqdm/events/events.py +148 -0
- mqdm/events/stream.py +236 -0
- mqdm/parallel/__init__.py +0 -0
- mqdm/parallel/_process_pool_keyboard.py +97 -0
- mqdm/parallel/apool.py +280 -0
- mqdm/parallel/executor.py +136 -0
- mqdm/parallel/pool.py +563 -0
- mqdm/runtime.py +497 -0
- mqdm/utils/__init__.py +180 -0
- mqdm/utils/_dev.py +147 -0
- mqdm/utils/_embed.py +41 -0
- mqdm/utils/_logging.py +123 -0
- mqdm/utils/proxy.py +386 -0
- mqdm-2.0.0.dist-info/METADATA +86 -0
- mqdm-2.0.0.dist-info/RECORD +24 -0
- mqdm-2.0.0.dist-info/WHEEL +5 -0
- mqdm-2.0.0.dist-info/top_level.txt +1 -0
mqdm/__init__.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from .runtime import Runtime, _current_runtime, configure
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def sustain():
|
|
5
|
+
"""Keep the live progress display alive across this block.
|
|
6
|
+
|
|
7
|
+
A sequence of separate bars normally renders one at a time — each freezes
|
|
8
|
+
into the scrollback as the next begins. Inside ``sustain()`` they stack and
|
|
9
|
+
stay visible together as one growing panel, with ``print``/logging streaming
|
|
10
|
+
above them. Nestable.
|
|
11
|
+
"""
|
|
12
|
+
return _current_runtime().sustain()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def print(*args, **kw):
|
|
16
|
+
"""Print above active progress bars."""
|
|
17
|
+
return _current_runtime().print(*args, **kw)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get(i=-1):
|
|
21
|
+
"""Get an mqdm instance."""
|
|
22
|
+
return _current_runtime().get_instance(i)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def set_description(desc, i=-1):
|
|
26
|
+
"""Set the description of the last progress bar."""
|
|
27
|
+
return get(i).set_description(desc)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def set(i=-1, **kw):
|
|
31
|
+
"""Set the last progress bar."""
|
|
32
|
+
return get(i).set(**kw)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def update(n=1, i=-1, **kw):
|
|
36
|
+
"""Update the last progress bar."""
|
|
37
|
+
return get(i).update(n, **kw)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def pause(paused=True):
|
|
41
|
+
"""Pause the progress bars. Useful for opening an interactive shell or printing stack traces."""
|
|
42
|
+
return _current_runtime().pause(paused)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------- Logging --------------------------------- #
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def install_logging(logger=None, *, level=None, capture_warnings='process', markup=True, formatter=None):
|
|
49
|
+
"""Install an MQDMHandler on a logger for a runtime."""
|
|
50
|
+
return _current_runtime().install_logging(
|
|
51
|
+
logger=logger,
|
|
52
|
+
level=level,
|
|
53
|
+
capture_warnings=capture_warnings,
|
|
54
|
+
markup=markup,
|
|
55
|
+
formatter=formatter,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def uninstall_logging(*, logger=None):
|
|
60
|
+
"""Remove an MQDMHandler from a logger for a runtime."""
|
|
61
|
+
return _current_runtime().uninstall_logging(logger=logger)
|
|
62
|
+
|
|
63
|
+
# ----------------------------------- Utils ---------------------------------- #
|
|
64
|
+
|
|
65
|
+
from . import utils
|
|
66
|
+
from .parallel.executor import T_POOL_MODE, get_executor, Initializer
|
|
67
|
+
from .utils import args, fn, fopen, ratelimit
|
|
68
|
+
from .utils._logging import MQDMHandler
|
|
69
|
+
|
|
70
|
+
# ---------------------------------- Events ---------------------------------- #
|
|
71
|
+
|
|
72
|
+
from . import events
|
|
73
|
+
|
|
74
|
+
# ----------------------------------- Core ----------------------------------- #
|
|
75
|
+
|
|
76
|
+
from .bar import mqdm
|
|
77
|
+
from .parallel.pool import ipool, pool, PoolError, Result
|
|
78
|
+
from .parallel.apool import aipool, apool
|
|
79
|
+
|
|
80
|
+
# ----------------------------- Development utils ---------------------------- #
|
|
81
|
+
|
|
82
|
+
from .utils._dev import bp, embed, iex, profile, timeit
|
|
83
|
+
|
|
84
|
+
__all__ = [
|
|
85
|
+
'mqdm',
|
|
86
|
+
'pool',
|
|
87
|
+
'ipool',
|
|
88
|
+
'apool',
|
|
89
|
+
'aipool',
|
|
90
|
+
'print',
|
|
91
|
+
'sustain',
|
|
92
|
+
'pause',
|
|
93
|
+
'args',
|
|
94
|
+
'PoolError',
|
|
95
|
+
'configure',
|
|
96
|
+
]
|
mqdm/backend/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .protocols import (
|
|
6
|
+
ProgressBackend,
|
|
7
|
+
ProgressBackendFactory,
|
|
8
|
+
ProxyConvertibleBackend,
|
|
9
|
+
RichTaskState,
|
|
10
|
+
TaskState,
|
|
11
|
+
)
|
|
12
|
+
from .protocols import ProgressBackendFactory as _PF # noqa: F401 — re-export
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_backend(
|
|
17
|
+
*,
|
|
18
|
+
runtime: Any,
|
|
19
|
+
**kw: Any,
|
|
20
|
+
) -> ProgressBackend:
|
|
21
|
+
from . import rich
|
|
22
|
+
|
|
23
|
+
return rich.Progress(**kw)
|
|
24
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Protocol, TypedDict, runtime_checkable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TaskState(TypedDict, total=False):
|
|
7
|
+
"""Minimum detached task snapshot contract shared across backends.
|
|
8
|
+
|
|
9
|
+
This is the state mqdm itself needs to reattach a logical task after a bar
|
|
10
|
+
is temporarily detached from a live backend.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
id: int
|
|
14
|
+
description: str
|
|
15
|
+
total: float | None
|
|
16
|
+
completed: float
|
|
17
|
+
visible: bool
|
|
18
|
+
fields: dict[str, Any]
|
|
19
|
+
start_time: float | None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RichTaskState(TaskState, total=False):
|
|
23
|
+
"""Rich-specific snapshot fields used for restore and progress snapshots."""
|
|
24
|
+
|
|
25
|
+
stop_time: float | None
|
|
26
|
+
finished_time: float | None
|
|
27
|
+
finished_speed: float | None
|
|
28
|
+
_progress: list[tuple[float, float]] | None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
#: Signature of a callable that creates a :class:`ProgressBackend`.
|
|
32
|
+
#:
|
|
33
|
+
#: ``runtime`` receives the calling :class:`mqdm.Runtime`. ``columns``
|
|
34
|
+
#: is an optional column-tuple that overrides the backend's own defaults.
|
|
35
|
+
#: Extra keyword arguments are forwarded to the backend constructor.
|
|
36
|
+
ProgressBackendFactory = Callable[..., "ProgressBackend"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ProgressBackend(Protocol):
|
|
40
|
+
"""Core runtime/backend contract used by mqdm bars.
|
|
41
|
+
|
|
42
|
+
This deliberately excludes optional capabilities such as process-mode
|
|
43
|
+
promotion or backend-specific snapshot extensions.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
multiprocess: bool
|
|
47
|
+
|
|
48
|
+
# ---------------------------------- Control --------------------------------- #
|
|
49
|
+
|
|
50
|
+
def start(self) -> None: ...
|
|
51
|
+
def stop(self) -> None: ...
|
|
52
|
+
def refresh(self) -> None: ...
|
|
53
|
+
|
|
54
|
+
# ---------------------------------- Output ---------------------------------- #
|
|
55
|
+
|
|
56
|
+
def write(self, *args: Any, **kw: Any) -> Any: ...
|
|
57
|
+
|
|
58
|
+
# ------------------------------ Task Management ----------------------------- #
|
|
59
|
+
|
|
60
|
+
def add_task(self, **task_kw: Any) -> int: ...
|
|
61
|
+
def try_update(self, task_id: int, **task_update: Any) -> None: ...
|
|
62
|
+
def dump_task(self, task_id: int) -> TaskState | None: ...
|
|
63
|
+
def load_task(self, task: TaskState, start: bool = True) -> None: ...
|
|
64
|
+
def pop_task(self, task_id: int, remove: bool | None = None) -> TaskState | None: ...
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@runtime_checkable
|
|
68
|
+
class ProxyConvertibleBackend(Protocol):
|
|
69
|
+
"""Optional capability for backends that can promote themselves for IPC."""
|
|
70
|
+
|
|
71
|
+
def convert_proxy(self, command_dispatch=None) -> ProgressBackend: ...
|
mqdm/backend/rich.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
from collections import deque
|
|
2
|
+
import dataclasses
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich import progress
|
|
8
|
+
import mqdm as M
|
|
9
|
+
|
|
10
|
+
from .protocols import RichTaskState, TaskState
|
|
11
|
+
from ..utils.proxy import TransportProxy, proxymethod
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _DiscardFile:
|
|
15
|
+
"""A write-only file-like object that discards all data.
|
|
16
|
+
|
|
17
|
+
Used as the Rich console file when ``silent=True`` so the in-memory
|
|
18
|
+
buffer doesn't grow without bounds across refreshes.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
encoding = "utf-8"
|
|
22
|
+
|
|
23
|
+
def write(self, s: str) -> int:
|
|
24
|
+
return len(s)
|
|
25
|
+
|
|
26
|
+
def flush(self) -> None:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
def getvalue(self) -> str:
|
|
30
|
+
return ""
|
|
31
|
+
|
|
32
|
+
def isatty(self) -> bool:
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Task(progress.Task):
|
|
37
|
+
@property
|
|
38
|
+
def speed(self):
|
|
39
|
+
"""Optional[float]: Get the estimated speed in steps per second."""
|
|
40
|
+
if self.start_time is None:
|
|
41
|
+
return None
|
|
42
|
+
with self._lock:
|
|
43
|
+
progress = self._progress
|
|
44
|
+
if progress:
|
|
45
|
+
total_time = progress[-1].timestamp - self.start_time
|
|
46
|
+
elif self.finished:
|
|
47
|
+
total_time = self.finished_time
|
|
48
|
+
else:
|
|
49
|
+
total_time = self.elapsed
|
|
50
|
+
return self.completed / total_time if total_time else total_time
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class TaskSnapshot:
|
|
55
|
+
id: int
|
|
56
|
+
description: str
|
|
57
|
+
total: float | None
|
|
58
|
+
completed: int
|
|
59
|
+
visible: bool
|
|
60
|
+
fields: dict
|
|
61
|
+
start_time: float | None = None
|
|
62
|
+
stop_time: float | None = None
|
|
63
|
+
finished_time: float | None = None
|
|
64
|
+
finished_speed: float | None = None
|
|
65
|
+
_progress: list[tuple[float, float]] | None = None
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_task(cls, task: progress.Task) -> 'TaskSnapshot':
|
|
69
|
+
data = {k.name: getattr(task, k.name) for k in dataclasses.fields(task) if not k.name.startswith('_')}
|
|
70
|
+
progress_samples = []
|
|
71
|
+
if task._progress:
|
|
72
|
+
progress_samples.append((task._progress[-1].timestamp, sum(s.completed for s in task._progress)))
|
|
73
|
+
return cls(**data, _progress=progress_samples)
|
|
74
|
+
|
|
75
|
+
def to_dict(self) -> RichTaskState:
|
|
76
|
+
return dataclasses.asdict(self)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_dict(cls, data: TaskState) -> 'TaskSnapshot':
|
|
80
|
+
return cls(**data)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class Progress(progress.Progress):
|
|
84
|
+
multiprocess = False
|
|
85
|
+
|
|
86
|
+
# ---------------------------------------------------------------------------- #
|
|
87
|
+
# Construction / Bootstrap #
|
|
88
|
+
# these hooks preserve enough init state to restore detached tasks and keep #
|
|
89
|
+
# process-safe proxy promotion independent of Rich's in-process-only state. #
|
|
90
|
+
# ---------------------------------------------------------------------------- #
|
|
91
|
+
|
|
92
|
+
def __init__(self, *, columns=None, _tasks=None, _task_index=None, _pause_event=None, silent=False, **kw):
|
|
93
|
+
if columns is None:
|
|
94
|
+
columns = self.default_progress_columns()
|
|
95
|
+
self._init_options = dict(kw)
|
|
96
|
+
if silent:
|
|
97
|
+
self._init_options['silent'] = True
|
|
98
|
+
kw.setdefault('console', Console(file=_DiscardFile(), force_terminal=True))
|
|
99
|
+
super().__init__(*(columns or ()), **kw)
|
|
100
|
+
|
|
101
|
+
if _tasks is not None:
|
|
102
|
+
self._tasks = {task_id: self._load_task(TaskSnapshot.from_dict(task)) for task_id, task in _tasks.items()}
|
|
103
|
+
self._task_index = progress.TaskID(_task_index or 0)
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def default_progress_columns(cls) -> tuple[object, ...]:
|
|
107
|
+
"""Return the default Rich column layout for mqdm progress displays.
|
|
108
|
+
|
|
109
|
+
Per-task fields such as ``bytes`` are resolved by the individual column
|
|
110
|
+
implementations, so the shared layout does not vary by runtime options.
|
|
111
|
+
"""
|
|
112
|
+
from . import rich_progress_columns
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
"[progress.description]{task.description}",
|
|
116
|
+
rich_progress_columns.TwoToneColumn(bar_width=None),
|
|
117
|
+
"[progress.percentage]{task.percentage:>3.0f}%",
|
|
118
|
+
rich_progress_columns.MofNColumn(),
|
|
119
|
+
rich_progress_columns.SpeedColumn(),
|
|
120
|
+
rich_progress_columns.TimeElapsedColumn(compact=True),
|
|
121
|
+
progress.TimeRemainingColumn(compact=True),
|
|
122
|
+
progress.SpinnerColumn(),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# -------------------------------- Rich API --------------------------------- #
|
|
126
|
+
|
|
127
|
+
def write(self, *args, **kw):
|
|
128
|
+
"""Print above the live progress display.
|
|
129
|
+
|
|
130
|
+
Uses this Progress's own console so the output interleaves with the
|
|
131
|
+
live region. When called through a process-safe proxy, the write is
|
|
132
|
+
routed into the process that owns the real ``Progress`` (and its Live).
|
|
133
|
+
"""
|
|
134
|
+
self.console.print(*args, **kw)
|
|
135
|
+
|
|
136
|
+
@wraps(progress.Progress.update)
|
|
137
|
+
def update(self, task_id, **kw):
|
|
138
|
+
if 'description' in kw and kw['description'] is None: # ignore None descriptions
|
|
139
|
+
kw.pop('description')
|
|
140
|
+
return super().update(task_id, **kw)
|
|
141
|
+
|
|
142
|
+
@wraps(progress.Progress.update)
|
|
143
|
+
def try_update(self, task_id, **kw):
|
|
144
|
+
try:
|
|
145
|
+
return self.update(task_id, **kw)
|
|
146
|
+
except KeyError as e:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
update_ = try_update
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------- #
|
|
152
|
+
# Task Lifecycle / Mutation #
|
|
153
|
+
# these overrides funnel task creation and lifecycle through mqdm-owned #
|
|
154
|
+
# helpers so local and proxied bars can share the same task state model. #
|
|
155
|
+
# ---------------------------------------------------------------------------- #
|
|
156
|
+
|
|
157
|
+
def _start_task(self, task):
|
|
158
|
+
if task.start_time is None:
|
|
159
|
+
task.start_time = self.get_time()
|
|
160
|
+
|
|
161
|
+
def _new_task(self,
|
|
162
|
+
description: str='',
|
|
163
|
+
start: bool = True,
|
|
164
|
+
total: float|None = None,
|
|
165
|
+
completed: int = 0,
|
|
166
|
+
visible: bool = True,
|
|
167
|
+
**fields):
|
|
168
|
+
task = Task(
|
|
169
|
+
self._task_index,
|
|
170
|
+
description,
|
|
171
|
+
total,
|
|
172
|
+
completed,
|
|
173
|
+
visible=visible,
|
|
174
|
+
fields=fields,
|
|
175
|
+
_get_time=self.get_time,
|
|
176
|
+
_lock=self._lock,
|
|
177
|
+
)
|
|
178
|
+
start and self._start_task(task)
|
|
179
|
+
self._task_index = progress.TaskID(int(self._task_index) + 1)
|
|
180
|
+
return task
|
|
181
|
+
|
|
182
|
+
def add_task(
|
|
183
|
+
self,
|
|
184
|
+
description: str='',
|
|
185
|
+
start: bool = True,
|
|
186
|
+
total: float|None = None,
|
|
187
|
+
completed: int = 0,
|
|
188
|
+
visible: bool = True,
|
|
189
|
+
**fields,
|
|
190
|
+
) -> progress.TaskID:
|
|
191
|
+
with self._lock:
|
|
192
|
+
task = self._new_task(description or '', start, total, completed, visible, **fields)
|
|
193
|
+
self._tasks[task.id] = task
|
|
194
|
+
self.refresh()
|
|
195
|
+
return task.id
|
|
196
|
+
|
|
197
|
+
def start_task(self, task_id: progress.TaskID) -> None:
|
|
198
|
+
with self._lock:
|
|
199
|
+
self._start_task(self._tasks[task_id])
|
|
200
|
+
|
|
201
|
+
def new_task(self,
|
|
202
|
+
description: str='',
|
|
203
|
+
start: bool = True,
|
|
204
|
+
total: float|None = None,
|
|
205
|
+
completed: int = 0,
|
|
206
|
+
visible: bool = True,
|
|
207
|
+
**fields):
|
|
208
|
+
with self._lock:
|
|
209
|
+
return self._dump_task(self._new_task(description or '', start, total, completed, visible, **fields)).to_dict()
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------- #
|
|
212
|
+
# Task Snapshot / Serialization #
|
|
213
|
+
# these helpers reduce Rich tasks to transportable snapshots so state can be #
|
|
214
|
+
# exported, restored, and forwarded across process boundaries. #
|
|
215
|
+
# ---------------------------------------------------------------------------- #
|
|
216
|
+
|
|
217
|
+
def dump_tasks(self) -> dict[int, RichTaskState]:
|
|
218
|
+
with self._lock:
|
|
219
|
+
return {task_id: self._dump_task(self._tasks[task_id]).to_dict() for task_id in self._tasks}
|
|
220
|
+
|
|
221
|
+
def dump_task(self, task_id) -> RichTaskState:
|
|
222
|
+
with self._lock:
|
|
223
|
+
return self._dump_task(self._tasks[task_id]).to_dict()
|
|
224
|
+
|
|
225
|
+
def _dump_task(self, task):
|
|
226
|
+
return TaskSnapshot.from_task(task)
|
|
227
|
+
|
|
228
|
+
def _load_task(self, snapshot: TaskSnapshot, start=True):
|
|
229
|
+
data = snapshot.to_dict()
|
|
230
|
+
_progress = data.pop('_progress') or []
|
|
231
|
+
start_time = data.pop('start_time', None)
|
|
232
|
+
stop_time = data.pop('stop_time', None)
|
|
233
|
+
task = Task(_get_time=self.get_time, _lock=self._lock, **data)
|
|
234
|
+
task.start_time = start_time
|
|
235
|
+
task.stop_time = stop_time
|
|
236
|
+
start and self._start_task(task)
|
|
237
|
+
if _progress:
|
|
238
|
+
task._progress = deque([progress.ProgressSample(*s) for s in _progress], maxlen=1000)
|
|
239
|
+
return task
|
|
240
|
+
|
|
241
|
+
def load_task(self, task: TaskState, start=True):
|
|
242
|
+
with self._lock:
|
|
243
|
+
task = self._load_task(TaskSnapshot.from_dict(task), start=start)
|
|
244
|
+
self._tasks[task.id] = task
|
|
245
|
+
if task.id >= self._task_index:
|
|
246
|
+
self._task_index = progress.TaskID(task.id+1)
|
|
247
|
+
self.refresh()
|
|
248
|
+
|
|
249
|
+
def pop_task(self, task_id, remove=None):
|
|
250
|
+
"""Close a task and return its serialized data."""
|
|
251
|
+
try:
|
|
252
|
+
self.stop_task(task_id)
|
|
253
|
+
data = self.dump_task(task_id)
|
|
254
|
+
if remove is None:
|
|
255
|
+
remove = self._tasks[task_id].fields.get('transient', False)
|
|
256
|
+
if remove:
|
|
257
|
+
self.remove_task(task_id)
|
|
258
|
+
return data
|
|
259
|
+
except KeyError as e:
|
|
260
|
+
pass
|
|
261
|
+
|
|
262
|
+
# ---------------------------------------------------------------------------- #
|
|
263
|
+
# Multiprocessing Backend Upgrade #
|
|
264
|
+
# this section upgrades a local Rich-backed Progress into a queue-backed #
|
|
265
|
+
# owner-rendered proxy for process workers. #
|
|
266
|
+
# ---------------------------------------------------------------------------- #
|
|
267
|
+
|
|
268
|
+
def convert_proxy(self, command_dispatch=None) -> 'ProgressProxy':
|
|
269
|
+
"""Convert to a multiprocessing-safe proxy object."""
|
|
270
|
+
return ProgressProxy.from_target(self, command_dispatch=command_dispatch)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class ProgressProxy(TransportProxy[Progress]):
|
|
274
|
+
multiprocess = True
|
|
275
|
+
|
|
276
|
+
start = proxymethod(Progress.start, expect_reply=False, owner_only=True)
|
|
277
|
+
stop = proxymethod(Progress.stop, expect_reply=False, owner_only=True)
|
|
278
|
+
refresh = proxymethod(Progress.refresh, expect_reply=False, owner_only=True)
|
|
279
|
+
|
|
280
|
+
write = proxymethod(Progress.write, expect_reply=False)
|
|
281
|
+
|
|
282
|
+
add_task = proxymethod(Progress.add_task)
|
|
283
|
+
try_update = proxymethod(Progress.try_update, expect_reply=False)
|
|
284
|
+
dump_task = proxymethod(Progress.dump_task)
|
|
285
|
+
load_task = proxymethod(Progress.load_task, expect_reply=False)
|
|
286
|
+
pop_task = proxymethod(Progress.pop_task)
|
|
287
|
+
|
|
288
|
+
# Unused methods (to deprecate) - not defined on ProgressBackend
|
|
289
|
+
|
|
290
|
+
new_task = proxymethod(Progress.new_task)
|
|
291
|
+
update = proxymethod(Progress.update, expect_reply=False)
|
|
292
|
+
update_ = try_update
|
|
293
|
+
start_task = proxymethod(Progress.start_task, expect_reply=False)
|
|
294
|
+
stop_task = proxymethod(Progress.stop_task, expect_reply=False)
|
|
295
|
+
remove_task = proxymethod(Progress.remove_task, expect_reply=False)
|
|
296
|
+
dump_tasks = proxymethod(Progress.dump_tasks)
|
|
297
|
+
|
|
298
|
+
def __rich_console__(self, console, options):
|
|
299
|
+
target = self.target
|
|
300
|
+
if target is None:
|
|
301
|
+
raise RuntimeError("ProgressProxy can only render in the owner process.")
|
|
302
|
+
yield target.get_renderable()
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from rich import progress
|
|
3
|
+
from rich.segment import Segment
|
|
4
|
+
from rich.style import Style
|
|
5
|
+
from rich.progress_bar import ProgressBar
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TwoToneBar(ProgressBar):
|
|
9
|
+
"""A progress bar with a lighter fill for work that's started but not done.
|
|
10
|
+
|
|
11
|
+
Renders three zones — completed, started-but-pending, and idle — so you can
|
|
12
|
+
see how far the workers have run ahead of what has actually finished. Reads
|
|
13
|
+
the pending count from ``started`` (defaults to ``completed``, i.e. a plain
|
|
14
|
+
bar) and clamps it to ``completed <= started <= total``.
|
|
15
|
+
|
|
16
|
+
The started zone defaults to a dimmed shade of the complete colour (same hue,
|
|
17
|
+
lower intensity). That reads as "same work, in progress" and — because dim is
|
|
18
|
+
an SGR attribute rather than a colour — stays distinct from the grey
|
|
19
|
+
background even on 16-colour terminals, where a lighter grey would collapse
|
|
20
|
+
into it. Pass ``started_style`` to override.
|
|
21
|
+
"""
|
|
22
|
+
def __init__(self, *args, started: float = 0, started_style: str | None = None, **kw):
|
|
23
|
+
super().__init__(*args, **kw)
|
|
24
|
+
self.started = started
|
|
25
|
+
self.started_style = started_style
|
|
26
|
+
|
|
27
|
+
def _resolve_started_style(self, console) -> Style:
|
|
28
|
+
if self.started_style is not None:
|
|
29
|
+
return console.get_style(self.started_style)
|
|
30
|
+
return console.get_style(self.complete_style) + Style(dim=True)
|
|
31
|
+
|
|
32
|
+
def __rich_console__(self, console, options):
|
|
33
|
+
width = min(self.width or options.max_width, options.max_width)
|
|
34
|
+
# Defer to the stock bar for indeterminate / pulsing tasks.
|
|
35
|
+
if self.total is None or self.pulse:
|
|
36
|
+
yield from super().__rich_console__(console, options)
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
bar = "-" if (options.legacy_windows or options.ascii_only) else "━"
|
|
40
|
+
done = max(0.0, min(self.total, self.completed))
|
|
41
|
+
started = max(done, min(self.total, self.started))
|
|
42
|
+
done_cells = int(round(width * done / self.total)) if self.total else 0
|
|
43
|
+
started_cells = int(round(width * started / self.total)) if self.total else 0
|
|
44
|
+
|
|
45
|
+
finished = self.total and self.completed >= self.total
|
|
46
|
+
zones = (
|
|
47
|
+
(done_cells, console.get_style(self.finished_style if finished else self.complete_style)),
|
|
48
|
+
(started_cells - done_cells, self._resolve_started_style(console)),
|
|
49
|
+
(width - started_cells, console.get_style(self.style)),
|
|
50
|
+
)
|
|
51
|
+
for count, style in zones:
|
|
52
|
+
if count > 0:
|
|
53
|
+
yield Segment(bar * count, style)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TwoToneColumn(progress.BarColumn):
|
|
57
|
+
"""``BarColumn`` that shades a task's ``started`` field as a second tone."""
|
|
58
|
+
def __init__(self, *args, started_style: str | None = None, **kw):
|
|
59
|
+
self.started_style = started_style
|
|
60
|
+
super().__init__(*args, **kw)
|
|
61
|
+
|
|
62
|
+
def render(self, task) -> ProgressBar:
|
|
63
|
+
return TwoToneBar(
|
|
64
|
+
total=max(0, task.total) if task.total is not None else None,
|
|
65
|
+
completed=max(0, task.completed),
|
|
66
|
+
started=max(0, task.fields.get("started", task.completed)),
|
|
67
|
+
width=None if self.bar_width is None else max(1, self.bar_width),
|
|
68
|
+
pulse=not task.started,
|
|
69
|
+
animation_time=task.get_time(),
|
|
70
|
+
style=self.style,
|
|
71
|
+
complete_style=self.complete_style,
|
|
72
|
+
finished_style=self.finished_style,
|
|
73
|
+
pulse_style=self.pulse_style,
|
|
74
|
+
started_style=self.started_style,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class MofNColumn(progress.DownloadColumn):
|
|
79
|
+
'''A progress column that shows the current vs. total count of items.'''
|
|
80
|
+
def __init__(self, bytes=False, separator="/", **kw):
|
|
81
|
+
self.bytes = bytes
|
|
82
|
+
self.separator = separator
|
|
83
|
+
super().__init__(**kw)
|
|
84
|
+
|
|
85
|
+
def render(self, task):
|
|
86
|
+
bytes = task.fields.get("bytes", self.bytes)
|
|
87
|
+
if bytes:
|
|
88
|
+
return super().render(task)
|
|
89
|
+
total = f'{int(task.total):,}' if task.total is not None else "?"
|
|
90
|
+
return progress.Text(
|
|
91
|
+
f"{int(task.completed):,d}{self.separator}{total}",
|
|
92
|
+
style="progress.download",
|
|
93
|
+
justify='right'
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class SpeedColumn(progress.TransferSpeedColumn):
|
|
98
|
+
"""Renders human readable transfer speed."""
|
|
99
|
+
def __init__(self, bytes=False, unit_scale=1, **kw):
|
|
100
|
+
self.bytes = bytes
|
|
101
|
+
self.unit_scale = unit_scale
|
|
102
|
+
super().__init__(**kw)
|
|
103
|
+
|
|
104
|
+
def render(self, task):
|
|
105
|
+
"""Show data transfer speed."""
|
|
106
|
+
bytes = task.fields.get("bytes", self.bytes)
|
|
107
|
+
if bytes:
|
|
108
|
+
return super().render(task)
|
|
109
|
+
speed = task.finished_speed or task.speed
|
|
110
|
+
if speed is None:
|
|
111
|
+
return progress.Text("", style="progress.data.speed")
|
|
112
|
+
end = '/s'
|
|
113
|
+
if 0 < speed < 1:
|
|
114
|
+
speed = 1 / speed
|
|
115
|
+
speed, suffix = time_units(speed)
|
|
116
|
+
end = '/x'
|
|
117
|
+
return progress.Text(f"{speed:.1f}{suffix}{end}", justify='right', style="progress.data.speed")
|
|
118
|
+
unit, suffix = progress.filesize.pick_unit_and_suffix(
|
|
119
|
+
int(speed),
|
|
120
|
+
["x", "K", "M", "B", "T"],
|
|
121
|
+
# ["", "×10³", "×10⁶", "×10⁹", "×10¹²"],
|
|
122
|
+
1000)
|
|
123
|
+
return progress.Text(f"{speed/unit:.1f}{suffix}{end}", justify='right', style="progress.data.speed")
|
|
124
|
+
|
|
125
|
+
def time_units(seconds):
|
|
126
|
+
for d, u in [(60, 's'), (60, 'm'), (24, 'h'), (365, 'd')]:
|
|
127
|
+
if seconds < d:
|
|
128
|
+
return seconds, u
|
|
129
|
+
seconds /= d
|
|
130
|
+
return seconds, 'y'
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TimeElapsedColumn(progress.TimeRemainingColumn):
|
|
134
|
+
"""Renders time elapsed."""
|
|
135
|
+
def __init__(self, compact: bool = False, **kw):
|
|
136
|
+
self._compact = compact
|
|
137
|
+
super().__init__(**kw)
|
|
138
|
+
|
|
139
|
+
def render(self, task):
|
|
140
|
+
"""Show time elapsed."""
|
|
141
|
+
elapsed = task.finished_time if task.finished else task.elapsed
|
|
142
|
+
if elapsed is None:
|
|
143
|
+
return progress.Text("--:--" if self._compact else "-:--:--", style="progress.elapsed")
|
|
144
|
+
minutes, seconds = divmod(int(elapsed), 60)
|
|
145
|
+
hours, minutes = divmod(minutes, 60)
|
|
146
|
+
return progress.Text(
|
|
147
|
+
(f"{hours:d}:" if hours or not self._compact else "") +
|
|
148
|
+
f"{minutes:02d}:{seconds:02d}", style="progress.elapsed")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# class LogBarColumn(progress.BarColumn):
|
|
153
|
+
# def render(self, task):
|
|
154
|
+
# return progress.Group(
|
|
155
|
+
# super().render(task),
|
|
156
|
+
# progress.Text(task.description, style="progress.description"),
|
|
157
|
+
# )
|
|
158
|
+
|