dcicutils 8.8.2.1b1__py3-none-any.whl → 8.8.2.1b3__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.
- dcicutils/progress_bar.py +281 -0
- {dcicutils-8.8.2.1b1.dist-info → dcicutils-8.8.2.1b3.dist-info}/METADATA +1 -1
- {dcicutils-8.8.2.1b1.dist-info → dcicutils-8.8.2.1b3.dist-info}/RECORD +6 -5
- {dcicutils-8.8.2.1b1.dist-info → dcicutils-8.8.2.1b3.dist-info}/LICENSE.txt +0 -0
- {dcicutils-8.8.2.1b1.dist-info → dcicutils-8.8.2.1b3.dist-info}/WHEEL +0 -0
- {dcicutils-8.8.2.1b1.dist-info → dcicutils-8.8.2.1b3.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,281 @@
|
|
1
|
+
from collections import namedtuple
|
2
|
+
from signal import signal, SIGINT
|
3
|
+
import sys
|
4
|
+
import threading
|
5
|
+
import time
|
6
|
+
from tqdm import tqdm
|
7
|
+
from types import FrameType as frame
|
8
|
+
from typing import Callable, Optional, Union
|
9
|
+
from contextlib import contextmanager
|
10
|
+
from dcicutils.command_utils import yes_or_no
|
11
|
+
|
12
|
+
|
13
|
+
class TQDM(tqdm):
|
14
|
+
|
15
|
+
"""
|
16
|
+
def moveto(self, n):
|
17
|
+
# Hack/workaround for apparent tqdm bug where (for example) if we use this twice
|
18
|
+
# in a row, and we interrupt (CTRL-C) the first one and abort the task, then the
|
19
|
+
# output for the second usage gets a bit garble; on the wrong line and whatnot;
|
20
|
+
# somehow, state is stuck across usages; can't quite see how from the tqdm code.
|
21
|
+
# This is a bit worrying but so far no other deleterious effects observed.
|
22
|
+
# This looks maybe promising:
|
23
|
+
return
|
24
|
+
"""
|
25
|
+
|
26
|
+
# Nevermind the above; found a more pointed solution from here:
|
27
|
+
# https://stackoverflow.com/questions/41707229/why-is-tqdm-printing-to-a-newline-instead-of-updating-the-same-line
|
28
|
+
# Why in the world would tqdm be maintaining state across instances?? Whatever, this fixes it.
|
29
|
+
def __init__(self, *args, **kwargs):
|
30
|
+
super()._instances.clear() if super()._instances else None
|
31
|
+
super().__init__(*args, **kwargs)
|
32
|
+
|
33
|
+
|
34
|
+
# Wrapper around tqdm command-line progress bar.
|
35
|
+
class ProgressBar:
|
36
|
+
|
37
|
+
@staticmethod
|
38
|
+
@contextmanager
|
39
|
+
def define(*args, **kwargs):
|
40
|
+
progress_bar = None
|
41
|
+
try:
|
42
|
+
progress_bar = ProgressBar(*args, **kwargs)
|
43
|
+
yield progress_bar
|
44
|
+
finally:
|
45
|
+
if progress_bar:
|
46
|
+
progress_bar.done()
|
47
|
+
|
48
|
+
def __init__(self, total: Optional[int] = None,
|
49
|
+
description: Optional[str] = None,
|
50
|
+
catch_interrupt: bool = True,
|
51
|
+
interrupt: Optional[Callable] = None,
|
52
|
+
interrupt_continue: Optional[Callable] = None,
|
53
|
+
interrupt_stop: Optional[Callable] = None,
|
54
|
+
interrupt_exit: bool = False,
|
55
|
+
interrupt_exit_message: Optional[Union[Callable, str]] = None,
|
56
|
+
interrupt_message: Optional[str] = None,
|
57
|
+
printf: Optional[Callable] = None,
|
58
|
+
tidy_output_hack: bool = True) -> None:
|
59
|
+
self._bar = None
|
60
|
+
self._disabled = False
|
61
|
+
self._done = False
|
62
|
+
self._printf = printf if callable(printf) else print
|
63
|
+
self._tidy_output_hack = (tidy_output_hack is True)
|
64
|
+
self._started = time.time()
|
65
|
+
self._stop_requested = False
|
66
|
+
# Interrupt handling. We do not do the actual (signal) interrupt setup
|
67
|
+
# in self._initialiaze as that could be called from a (sub) thread; and in
|
68
|
+
# Python we can only set a signal (SIGINT in our case) on the main thread.
|
69
|
+
self._catch_interrupt = (catch_interrupt is True)
|
70
|
+
self._interrupt = interrupt if callable(interrupt) else None
|
71
|
+
self._interrupt_continue = interrupt_continue if callable(interrupt_continue) else None
|
72
|
+
self._interrupt_stop = interrupt_stop if callable(interrupt_stop) else None
|
73
|
+
if interrupt_exit in [True, False]:
|
74
|
+
if not self._interrupt_stop:
|
75
|
+
self._interrupt_stop = lambda _: interrupt_exit
|
76
|
+
self._interrupt_exit = interrupt_exit
|
77
|
+
else:
|
78
|
+
self._interrupt_exit = None
|
79
|
+
self._interrupt_message = interrupt_message if isinstance(interrupt_message, str) else None
|
80
|
+
if isinstance(interrupt_exit_message, str):
|
81
|
+
self._interrupt_exit_message = lambda bar: interrupt_exit_message
|
82
|
+
elif isinstance(interrupt_exit_message, Callable):
|
83
|
+
self._interrupt_exit_message = interrupt_exit_message
|
84
|
+
else:
|
85
|
+
self._interrupt_exit_message = None
|
86
|
+
self._interrupt_handler = None
|
87
|
+
if self._catch_interrupt:
|
88
|
+
self._interrupt_handler = self._define_interrupt_handler()
|
89
|
+
if self._tidy_output_hack is True:
|
90
|
+
self._tidy_output_hack = self._define_tidy_output_hack()
|
91
|
+
self._total = total if isinstance(total, int) and total >= 0 else 0
|
92
|
+
self._description = self._format_description(description)
|
93
|
+
# self._initialize()
|
94
|
+
|
95
|
+
def _initialize(self) -> bool:
|
96
|
+
# Do not actually create the tqdm object unless/until we have a positive total.
|
97
|
+
if (self._bar is None) and (self._total > 0):
|
98
|
+
bar_format = "{l_bar}{bar}| {n_fmt}/{total_fmt} | {rate_fmt} | {elapsed}{postfix} | ETA: {remaining} "
|
99
|
+
self._bar = TQDM(total=self._total, desc=self._description,
|
100
|
+
dynamic_ncols=True, bar_format=bar_format, unit="", file=sys.stdout)
|
101
|
+
if self._disabled:
|
102
|
+
self._bar.disable = True
|
103
|
+
return True
|
104
|
+
return False
|
105
|
+
|
106
|
+
def set_total(self, value: int, reset_eta: bool = False) -> None:
|
107
|
+
if value == self._total:
|
108
|
+
# If the total has not changed since last set then do nothing.
|
109
|
+
if reset_eta and self._bar is not None:
|
110
|
+
self._bar.reset()
|
111
|
+
return
|
112
|
+
if isinstance(value, int) and value > 0:
|
113
|
+
self._total = value
|
114
|
+
if self._bar is not None:
|
115
|
+
# This reset is needed to get the ETA to reset properly when we reset
|
116
|
+
# the total during the course of a single ProgressBar instance.
|
117
|
+
self._bar.reset()
|
118
|
+
self._bar.total = value
|
119
|
+
|
120
|
+
def reset_eta(self) -> None:
|
121
|
+
# Since set_total does nothing if total is the same, provide
|
122
|
+
# a way to reset the ETA if starting over with the same total.
|
123
|
+
if self._bar is not None:
|
124
|
+
self._bar.reset()
|
125
|
+
|
126
|
+
def set_progress(self, value: int) -> None:
|
127
|
+
if isinstance(value, int) and value >= 0:
|
128
|
+
if (self._bar is not None) or self._initialize():
|
129
|
+
self._bar.n = value
|
130
|
+
self._bar.refresh()
|
131
|
+
|
132
|
+
def increment_progress(self, value: int) -> None:
|
133
|
+
if isinstance(value, int) and value > 0:
|
134
|
+
if (self._bar is not None) or self._initialize():
|
135
|
+
self._bar.update(value)
|
136
|
+
self._bar.refresh()
|
137
|
+
|
138
|
+
def set_description(self, value: str) -> None:
|
139
|
+
self._description = self._format_description(value)
|
140
|
+
if self._bar is not None:
|
141
|
+
self._bar.set_description(self._description)
|
142
|
+
|
143
|
+
def done(self) -> None:
|
144
|
+
if self._done or self._bar is None:
|
145
|
+
return
|
146
|
+
self._ended = time.time()
|
147
|
+
self._done = True
|
148
|
+
self.set_progress(self.total) # xyzzy
|
149
|
+
self._bar.set_description(self._description)
|
150
|
+
self._bar.refresh()
|
151
|
+
# FYI: Do NOT do a bar.disable = True before a bar.close() or it messes up output
|
152
|
+
# on multiple calls; found out the hard way; a couple hours will never get back :-/
|
153
|
+
self._bar.close()
|
154
|
+
if self._tidy_output_hack:
|
155
|
+
self._tidy_output_hack.restore()
|
156
|
+
if self._interrupt_handler:
|
157
|
+
self._interrupt_handler.restore()
|
158
|
+
|
159
|
+
def disable(self, value: bool = True) -> None:
|
160
|
+
self._disabled = (value is True)
|
161
|
+
if self._bar is not None:
|
162
|
+
self._bar.disable = self._disabled
|
163
|
+
|
164
|
+
def enable(self, value: bool = True) -> None:
|
165
|
+
self.disable(not value)
|
166
|
+
|
167
|
+
@property
|
168
|
+
def total(self) -> int:
|
169
|
+
return self._bar.total if self._bar else 0
|
170
|
+
|
171
|
+
@property
|
172
|
+
def progress(self) -> int:
|
173
|
+
return self._bar.n if self._bar else 0
|
174
|
+
|
175
|
+
@property
|
176
|
+
def disabled(self) -> bool:
|
177
|
+
return self._disabled
|
178
|
+
|
179
|
+
@property
|
180
|
+
def enabled(self) -> bool:
|
181
|
+
return not self.disabled
|
182
|
+
|
183
|
+
@property
|
184
|
+
def stop_requested(self) -> bool:
|
185
|
+
return self._stop_requested
|
186
|
+
|
187
|
+
@property
|
188
|
+
def started(self) -> None:
|
189
|
+
return self._started
|
190
|
+
|
191
|
+
@property
|
192
|
+
def duration(self) -> None:
|
193
|
+
return time.time() - self._started
|
194
|
+
|
195
|
+
def _format_description(self, value: str) -> str:
|
196
|
+
if not isinstance(value, str):
|
197
|
+
value = ""
|
198
|
+
if self._tidy_output_hack and not value.endswith(self._tidy_output_hack.sentinel):
|
199
|
+
value += self._tidy_output_hack.sentinel
|
200
|
+
return value
|
201
|
+
|
202
|
+
def _define_interrupt_handler(self) -> None:
|
203
|
+
def handle_interrupt(signum: int, frame: frame) -> None: # noqa
|
204
|
+
nonlocal self
|
205
|
+
def handle_secondary_interrupt(signum: int, frame: frame) -> None: # noqa
|
206
|
+
nonlocal self
|
207
|
+
self._printf("\nEnter 'yes' or 'no' or CTRL-\\ to completely abort ...")
|
208
|
+
self.disable()
|
209
|
+
self._interrupt(self) if self._interrupt else None
|
210
|
+
set_interrupt_handler(handle_secondary_interrupt)
|
211
|
+
if yes_or_no(f"\nALERT! You have interrupted this {self._interrupt_message or 'process'}."
|
212
|
+
f" Do you want to stop{' (exit)' if self._interrupt_exit else ''}?"):
|
213
|
+
# Here there was an interrupt (CTRL-C) and the user confirmed (yes)
|
214
|
+
# that they want to stop the process; if the interrupt_stop handler
|
215
|
+
# is defined and returns True, then we exit the entire process here,
|
216
|
+
# rather than simply returning, which is the default.
|
217
|
+
if self._interrupt_stop:
|
218
|
+
interrupt_stop = self._interrupt_stop(self)
|
219
|
+
if (interrupt_stop is True) or ((interrupt_stop is None) and (self._interrupt_exit is True)):
|
220
|
+
self.done()
|
221
|
+
restore_interrupt_handler()
|
222
|
+
if self._interrupt_exit_message:
|
223
|
+
if isinstance(interrupt_exit_message := self._interrupt_exit_message(self), str):
|
224
|
+
self._printf(interrupt_exit_message)
|
225
|
+
exit(1)
|
226
|
+
elif interrupt_stop is False or ((interrupt_stop is None) and (self._interrupt_exit is False)):
|
227
|
+
set_interrupt_handler(handle_interrupt)
|
228
|
+
interrupt_continue = self._interrupt_continue(self) if self._interrupt_continue else None
|
229
|
+
if not (interrupt_continue is False):
|
230
|
+
self.enable()
|
231
|
+
return
|
232
|
+
self._stop_requested = True
|
233
|
+
return
|
234
|
+
set_interrupt_handler(handle_interrupt)
|
235
|
+
self._interrupt_continue(self) if self._interrupt_continue else None
|
236
|
+
self.enable()
|
237
|
+
def restore_interrupt_handler() -> None: # noqa
|
238
|
+
nonlocal self, previous_interrupt_handler
|
239
|
+
set_interrupt_handler(previous_interrupt_handler)
|
240
|
+
def set_interrupt_handler(interrupt_handler: Callable) -> Optional[Callable]: # noqa
|
241
|
+
nonlocal self
|
242
|
+
if callable(interrupt_handler) and (threading.current_thread() == threading.main_thread()):
|
243
|
+
return signal(SIGINT, interrupt_handler)
|
244
|
+
return None
|
245
|
+
previous_interrupt_handler = set_interrupt_handler(handle_interrupt)
|
246
|
+
return namedtuple("interrupt_handler", ["restore"])(restore_interrupt_handler)
|
247
|
+
|
248
|
+
def _define_tidy_output_hack(self) -> None:
|
249
|
+
# Some minor tqdm output tidying-up which for annoying anomalies; tqdm forces
|
250
|
+
# a colon (:) before the percentage, e.g. ": 25%|"; and while we're at it do
|
251
|
+
# a little ASCII progress animation, requiring a special ([progress]) sentinal
|
252
|
+
# string in the display string where the progress bar should actually go,
|
253
|
+
# which we do in _format_description. Other minor things too; see below.
|
254
|
+
sys_stdout_write = sys.stdout.write
|
255
|
+
def tidy_stdout_write(text: str) -> None: # noqa
|
256
|
+
nonlocal self, sys_stdout_write, sentinel_internal, spina, spini, spinn
|
257
|
+
def replace_first(value: str, match: str, replacement: str) -> str: # noqa
|
258
|
+
return value[:i] + replacement + value[i + len(match):] if (i := value.find(match)) >= 0 else value
|
259
|
+
if (self._disabled or self._done) and sentinel_internal in text:
|
260
|
+
# Another hack to really disable output on interrupt; in this case we set
|
261
|
+
# tqdm.disable to True, but output can still dribble out, so if the output
|
262
|
+
# looks like it is from tqdm and we are disabled/done then do no output.
|
263
|
+
return
|
264
|
+
if sentinel_internal in text:
|
265
|
+
spinc = spina[spini % spinn] if not ("100%|" in text) else "| ✓" ; spini += 1 # noqa
|
266
|
+
text = replace_first(text, sentinel_internal, f" {spinc}")
|
267
|
+
text = replace_first(text, "%|", "% ◀|")
|
268
|
+
# Another oddity: for the rate sometimes tqdm intermittently prints
|
269
|
+
# something like "1.54s/" rather than "1.54/s"; something to do with
|
270
|
+
# the unit we gave, which is empty; idunno; just replace it here.
|
271
|
+
text = replace_first(text, "s/ ", "/s ")
|
272
|
+
sys_stdout_write(text)
|
273
|
+
sys.stdout.flush()
|
274
|
+
def restore_stdout_write() -> None: # noqa
|
275
|
+
nonlocal sys_stdout_write
|
276
|
+
if sys_stdout_write is not None:
|
277
|
+
sys.stdout.write = sys_stdout_write
|
278
|
+
sys.stdout.write = tidy_stdout_write
|
279
|
+
spina = ["|", "/", "—", "◦", "\\"] ; spini = 0 ; spinn = len(spina) # noqa
|
280
|
+
sentinel = "[progress]" ; sentinel_internal = f"{sentinel}:" # noqa
|
281
|
+
return namedtuple("tidy_output_hack", ["restore", "sentinel"])(restore_stdout_write, sentinel)
|
@@ -48,6 +48,7 @@ dcicutils/obfuscation_utils.py,sha256=fo2jOmDRC6xWpYX49u80bVNisqRRoPskFNX3ymFAmj
|
|
48
48
|
dcicutils/opensearch_utils.py,sha256=V2exmFYW8Xl2_pGFixF4I2Cc549Opwe4PhFi5twC0M8,1017
|
49
49
|
dcicutils/portal_object_utils.py,sha256=gDXRgPsRvqCFwbC8WatsuflAxNiigOnqr0Hi93k3AgE,15422
|
50
50
|
dcicutils/portal_utils.py,sha256=oBoI3KWRp6YrbsuVGbmPQ3kATB5cVVsQo7-qmnYXWqg,30260
|
51
|
+
dcicutils/progress_bar.py,sha256=vQeVz6TMM5OkCWsQ1EDUMKpvbjvp7SshmOxIHFIQnbU,13384
|
51
52
|
dcicutils/project_utils.py,sha256=qPdCaFmWUVBJw4rw342iUytwdQC0P-XKpK4mhyIulMM,31250
|
52
53
|
dcicutils/qa_checkers.py,sha256=cdXjeL0jCDFDLT8VR8Px78aS10hwNISOO5G_Zv2TZ6M,20534
|
53
54
|
dcicutils/qa_utils.py,sha256=TT0SiJWiuxYvbsIyhK9VO4uV_suxhB6CpuC4qPacCzQ,160208
|
@@ -71,8 +72,8 @@ dcicutils/trace_utils.py,sha256=g8kwV4ebEy5kXW6oOrEAUsurBcCROvwtZqz9fczsGRE,1769
|
|
71
72
|
dcicutils/validation_utils.py,sha256=cMZIU2cY98FYtzK52z5WUYck7urH6JcqOuz9jkXpqzg,14797
|
72
73
|
dcicutils/variant_utils.py,sha256=2H9azNx3xAj-MySg-uZ2SFqbWs4kZvf61JnK6b-h4Qw,4343
|
73
74
|
dcicutils/zip_utils.py,sha256=rnjNv_k6L9jT2SjDSgVXp4BEJYLtz9XN6Cl2Fy-tqnM,2027
|
74
|
-
dcicutils-8.8.2.
|
75
|
-
dcicutils-8.8.2.
|
76
|
-
dcicutils-8.8.2.
|
77
|
-
dcicutils-8.8.2.
|
78
|
-
dcicutils-8.8.2.
|
75
|
+
dcicutils-8.8.2.1b3.dist-info/LICENSE.txt,sha256=qnwSmfnEWMl5l78VPDEzAmEbLVrRqQvfUQiHT0ehrOo,1102
|
76
|
+
dcicutils-8.8.2.1b3.dist-info/METADATA,sha256=uWWYit7e1yh-LNLXIOtDXxPyDTi2mIB62N112vQJaas,3356
|
77
|
+
dcicutils-8.8.2.1b3.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
|
78
|
+
dcicutils-8.8.2.1b3.dist-info/entry_points.txt,sha256=51Q4F_2V10L0282W7HFjP4jdzW4K8lnWDARJQVFy_hw,270
|
79
|
+
dcicutils-8.8.2.1b3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|