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 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
+ ]
@@ -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
+