cs-progress 20250306__py2.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.
- cs/progress.py +1148 -0
- cs_progress-20250306.dist-info/METADATA +617 -0
- cs_progress-20250306.dist-info/RECORD +4 -0
- cs_progress-20250306.dist-info/WHEEL +5 -0
cs/progress.py
ADDED
|
@@ -0,0 +1,1148 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Progress counting.
|
|
4
|
+
# - Cameron Simpson <cs@cskk.id.au> 15feb2015
|
|
5
|
+
#
|
|
6
|
+
# pylint: disable=too-many-lines
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
''' A progress tracker with methods for throughput, ETA and update notification;
|
|
10
|
+
also a compound progress meter composed from other progress meters.
|
|
11
|
+
'''
|
|
12
|
+
|
|
13
|
+
from collections import namedtuple
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
import functools
|
|
16
|
+
import sys
|
|
17
|
+
from threading import RLock, Thread
|
|
18
|
+
import time
|
|
19
|
+
from typing import Callable, Optional
|
|
20
|
+
|
|
21
|
+
from icontract import ensure
|
|
22
|
+
from typeguard import typechecked
|
|
23
|
+
|
|
24
|
+
from cs.deco import decorator, uses_quiet
|
|
25
|
+
from cs.logutils import debug, exception
|
|
26
|
+
from cs.py.func import funcname
|
|
27
|
+
from cs.queues import IterableQueue, QueueIterator
|
|
28
|
+
from cs.seq import seq
|
|
29
|
+
from cs.threads import bg
|
|
30
|
+
from cs.units import (
|
|
31
|
+
transcribe_time,
|
|
32
|
+
transcribe as transcribe_units,
|
|
33
|
+
BINARY_BYTES_SCALE,
|
|
34
|
+
DECIMAL_SCALE,
|
|
35
|
+
TIME_SCALE,
|
|
36
|
+
UNSCALED_SCALE,
|
|
37
|
+
)
|
|
38
|
+
from cs.upd import Upd, uses_upd, print # pylint: disable=redefined-builtin
|
|
39
|
+
|
|
40
|
+
__version__ = '20250306'
|
|
41
|
+
|
|
42
|
+
DISTINFO = {
|
|
43
|
+
'keywords': ["python2", "python3"],
|
|
44
|
+
'classifiers': [
|
|
45
|
+
"Programming Language :: Python",
|
|
46
|
+
"Programming Language :: Python :: 3",
|
|
47
|
+
],
|
|
48
|
+
'install_requires': [
|
|
49
|
+
'cs.deco',
|
|
50
|
+
'cs.logutils',
|
|
51
|
+
'cs.py.func',
|
|
52
|
+
'cs.seq',
|
|
53
|
+
'cs.threads',
|
|
54
|
+
'cs.units',
|
|
55
|
+
'cs.upd',
|
|
56
|
+
'icontract',
|
|
57
|
+
'typeguard',
|
|
58
|
+
],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# default to 5s of position buffer for computing recent thoroughput
|
|
62
|
+
DEFAULT_THROUGHPUT_WINDOW = 5
|
|
63
|
+
|
|
64
|
+
# default update period
|
|
65
|
+
DEFAULT_UPDATE_PERIOD = 0.3
|
|
66
|
+
|
|
67
|
+
@functools.total_ordering
|
|
68
|
+
class BaseProgress(object):
|
|
69
|
+
''' The base class for `Progress` and `OverProcess`
|
|
70
|
+
with various common methods.
|
|
71
|
+
|
|
72
|
+
Note that durations are in seconds
|
|
73
|
+
and that absolute time is in seconds since the UNIX epoch
|
|
74
|
+
(the basis of `time.time()`).
|
|
75
|
+
'''
|
|
76
|
+
|
|
77
|
+
def __init__(self, name=None, start_time=None, units_scale=None):
|
|
78
|
+
''' Initialise a progress instance.
|
|
79
|
+
|
|
80
|
+
Parameters:
|
|
81
|
+
* `name`: optional name
|
|
82
|
+
* `start_time`: optional UNIX epoch start time, default from `time.time()`
|
|
83
|
+
* `units_scale`: a scale for use with `cs.units.transcribe`,
|
|
84
|
+
default `BINARY_BYTES_SCALE`
|
|
85
|
+
'''
|
|
86
|
+
now = time.time()
|
|
87
|
+
if name is None:
|
|
88
|
+
name = '-'.join((type(self).__name__, str(seq())))
|
|
89
|
+
if start_time is None:
|
|
90
|
+
start_time = now
|
|
91
|
+
elif start_time > now:
|
|
92
|
+
raise ValueError("start_time(%s) > now(%s)" % (start_time, now))
|
|
93
|
+
if units_scale is None:
|
|
94
|
+
units_scale = BINARY_BYTES_SCALE
|
|
95
|
+
self.name = name
|
|
96
|
+
self.start_time = start_time
|
|
97
|
+
self.units_scale = units_scale
|
|
98
|
+
self.notify_update = set()
|
|
99
|
+
self._warned = set()
|
|
100
|
+
self._lock = RLock()
|
|
101
|
+
|
|
102
|
+
def __str__(self):
|
|
103
|
+
return "%s[start=%s:pos=%s:total=%s]" \
|
|
104
|
+
% (self.name, self.start, self.position, self.total)
|
|
105
|
+
|
|
106
|
+
__repr__ = __str__
|
|
107
|
+
|
|
108
|
+
def __int__(self):
|
|
109
|
+
''' `int(Progress)` returns the current position.
|
|
110
|
+
'''
|
|
111
|
+
return self.position
|
|
112
|
+
|
|
113
|
+
def __eq__(self, other):
|
|
114
|
+
''' A Progress is equal to another object `other`
|
|
115
|
+
if its position equals `int(other)`.
|
|
116
|
+
'''
|
|
117
|
+
return int(self) == int(other)
|
|
118
|
+
|
|
119
|
+
def __lt__(self, other):
|
|
120
|
+
''' A Progress is less then another object `other`
|
|
121
|
+
if its position is less than `int(other)`.
|
|
122
|
+
'''
|
|
123
|
+
return int(self) < int(other)
|
|
124
|
+
|
|
125
|
+
def __hash__(self):
|
|
126
|
+
return id(self)
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def elapsed_time(self):
|
|
130
|
+
''' Time elapsed since `start_time`.
|
|
131
|
+
'''
|
|
132
|
+
return time.time() - self.start_time
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def ratio(self):
|
|
136
|
+
''' The fraction of progress completed: `(position-start)/(total-start)`.
|
|
137
|
+
Returns `None` if `total` is `None` or `total<=start`.
|
|
138
|
+
|
|
139
|
+
Example:
|
|
140
|
+
|
|
141
|
+
>>> P = Progress()
|
|
142
|
+
P.ratio
|
|
143
|
+
>>> P.total = 16
|
|
144
|
+
>>> P.ratio
|
|
145
|
+
0.0
|
|
146
|
+
>>> P.update(4)
|
|
147
|
+
>>> P.ratio
|
|
148
|
+
0.25
|
|
149
|
+
'''
|
|
150
|
+
total = self.total
|
|
151
|
+
if total is None:
|
|
152
|
+
return None
|
|
153
|
+
start = self.start
|
|
154
|
+
if total <= start:
|
|
155
|
+
return None
|
|
156
|
+
return float(self.position - start) / (total - start)
|
|
157
|
+
|
|
158
|
+
def throughput_recent(self, time_window):
|
|
159
|
+
''' The recent throughput. Implemented by subclasses.
|
|
160
|
+
'''
|
|
161
|
+
raise NotImplementedError(
|
|
162
|
+
"%s.throughtput_recent(time_window=%s): subclass must implement" %
|
|
163
|
+
(type(self).__name__, time_window)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def throughput_overall(self):
|
|
167
|
+
''' The overall throughput from `start` to `position`
|
|
168
|
+
during `elapsed_time`.
|
|
169
|
+
'''
|
|
170
|
+
consumed = self.position - self.start
|
|
171
|
+
if consumed < 0:
|
|
172
|
+
debug(
|
|
173
|
+
"%s.throughput: self.position(%s) < self.start(%s)", self,
|
|
174
|
+
self.position, self.start
|
|
175
|
+
)
|
|
176
|
+
if consumed == 0:
|
|
177
|
+
return 0
|
|
178
|
+
elapsed = self.elapsed_time
|
|
179
|
+
if elapsed == 0:
|
|
180
|
+
return 0
|
|
181
|
+
if elapsed <= 0:
|
|
182
|
+
debug(
|
|
183
|
+
"%s.throughput: negative elapsed time since start_time=%s: %s", self,
|
|
184
|
+
self.start_time, elapsed
|
|
185
|
+
)
|
|
186
|
+
return 0
|
|
187
|
+
return float(consumed) / elapsed
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def throughput(self):
|
|
191
|
+
''' The overall throughput: `self.throughput_overall()`.
|
|
192
|
+
|
|
193
|
+
By comparison,
|
|
194
|
+
the `Progress.throughput` property is `self.throughput_recent`
|
|
195
|
+
if the `throughput_window` is not `None`,
|
|
196
|
+
otherwise it falls back to `throughput_overall`.
|
|
197
|
+
'''
|
|
198
|
+
return self.throughput_overall()
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def remaining_time(self):
|
|
202
|
+
''' The projected time remaining to end
|
|
203
|
+
based on the `throughput` and `total`.
|
|
204
|
+
|
|
205
|
+
If `total` is `None`, this is `None`.
|
|
206
|
+
'''
|
|
207
|
+
total = self.total
|
|
208
|
+
if total is None:
|
|
209
|
+
return None
|
|
210
|
+
remaining = total - self.position
|
|
211
|
+
if remaining < 0:
|
|
212
|
+
if "position>total" not in self._warned:
|
|
213
|
+
self._warned.add("position>total")
|
|
214
|
+
debug(
|
|
215
|
+
"%s.remaining_time: self.position(%s) > self.total(%s)", self,
|
|
216
|
+
self.position, self.total
|
|
217
|
+
)
|
|
218
|
+
return None
|
|
219
|
+
throughput = self.throughput
|
|
220
|
+
if throughput is None or throughput == 0:
|
|
221
|
+
return None
|
|
222
|
+
return remaining / throughput
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def eta(self):
|
|
226
|
+
''' The projected time of completion: now + `remaining_time`.
|
|
227
|
+
|
|
228
|
+
If `remaining_time` is `None`, this is also `None`.
|
|
229
|
+
'''
|
|
230
|
+
remaining = self.remaining_time
|
|
231
|
+
if remaining is None:
|
|
232
|
+
return None
|
|
233
|
+
return time.time() + remaining
|
|
234
|
+
|
|
235
|
+
@ensure(lambda width, result: len(result) <= width)
|
|
236
|
+
def arrow(self, width, no_padding=False):
|
|
237
|
+
''' Construct a progress arrow representing completion
|
|
238
|
+
to fit in the specified `width`.
|
|
239
|
+
'''
|
|
240
|
+
if width < 1:
|
|
241
|
+
return ''
|
|
242
|
+
ratio = self.ratio
|
|
243
|
+
if ratio is None or ratio <= 0:
|
|
244
|
+
arrow = ''
|
|
245
|
+
elif ratio < 1.0:
|
|
246
|
+
arrow_len = width * ratio
|
|
247
|
+
if arrow_len < 1:
|
|
248
|
+
arrow = '>'
|
|
249
|
+
else:
|
|
250
|
+
arrow = '=' * int(arrow_len - 1) + '>'
|
|
251
|
+
else:
|
|
252
|
+
arrow = '=' * width
|
|
253
|
+
if not no_padding:
|
|
254
|
+
arrow += ' ' * (width - len(arrow))
|
|
255
|
+
return arrow
|
|
256
|
+
|
|
257
|
+
def format_counter(self, value, scale=None, max_parts=2, sep=',', **kw):
|
|
258
|
+
''' Format `value` accoridng to `scale` and `max_parts`
|
|
259
|
+
using `cs.units.transcribe`.
|
|
260
|
+
'''
|
|
261
|
+
if scale is None:
|
|
262
|
+
scale = self.units_scale
|
|
263
|
+
if scale is None:
|
|
264
|
+
return str(value)
|
|
265
|
+
return transcribe_units(value, scale, max_parts=max_parts, sep=sep, **kw)
|
|
266
|
+
|
|
267
|
+
def text_pos_of_total(
|
|
268
|
+
self, fmt=None, fmt_pos=None, fmt_total=None, pos_first=False
|
|
269
|
+
):
|
|
270
|
+
''' Return a "total:position" or "position/total" style progress string.
|
|
271
|
+
|
|
272
|
+
Parameters:
|
|
273
|
+
* `fmt`: format string interpolating `pos_text` and `total_text`.
|
|
274
|
+
Default: `"{pos_text}/{total_text}"` if `pos_first`,
|
|
275
|
+
otherwise `"{total_text}:{pos_text}"`
|
|
276
|
+
* `fmt_pos`: formatting function for `self.position`,
|
|
277
|
+
default `self.format_counter`
|
|
278
|
+
* `fmt_total`: formatting function for `self.total`,
|
|
279
|
+
default from `fmt_pos`
|
|
280
|
+
* `pos_first`: put the position first if true (default `False`),
|
|
281
|
+
only consulted if `fmt` is `None`
|
|
282
|
+
'''
|
|
283
|
+
if fmt_pos is None:
|
|
284
|
+
fmt_pos = self.format_counter
|
|
285
|
+
if fmt_total is None:
|
|
286
|
+
fmt_total = fmt_pos
|
|
287
|
+
if fmt is None:
|
|
288
|
+
fmt = "{pos_text}/{total_text}" if pos_first else "{total_text}:{pos_text}"
|
|
289
|
+
pos_text = fmt_pos(self.position)
|
|
290
|
+
total_text = fmt_pos(self.total)
|
|
291
|
+
return fmt.format(pos_text=pos_text, total_text=total_text)
|
|
292
|
+
|
|
293
|
+
# pylint: disable=too-many-branches,too-many-statements
|
|
294
|
+
def status(self, label, width, recent_window=None, stalled=None):
|
|
295
|
+
''' A progress string of the form:
|
|
296
|
+
*label*`: `*pos*`/`*total*` ==> ETA '*time*
|
|
297
|
+
|
|
298
|
+
Parameters:
|
|
299
|
+
* `label`: the label for the status line;
|
|
300
|
+
if `None` use `self.name`
|
|
301
|
+
* `width`: the available width for the status line;
|
|
302
|
+
if not an `int` use `width.width`
|
|
303
|
+
* `recent_window`: optional timeframe to define "recent" in seconds,
|
|
304
|
+
default : `5`
|
|
305
|
+
* `stalled`: the label to indicate no throughput, default `'stalled'`;
|
|
306
|
+
for a worker this might often b better as `'idle'`
|
|
307
|
+
'''
|
|
308
|
+
if label is None:
|
|
309
|
+
label = self.name
|
|
310
|
+
if stalled is None:
|
|
311
|
+
stalled = 'stalled'
|
|
312
|
+
if not isinstance(width, int):
|
|
313
|
+
width = width.width
|
|
314
|
+
if recent_window is None:
|
|
315
|
+
recent_window = 5
|
|
316
|
+
leftv = []
|
|
317
|
+
rightv = []
|
|
318
|
+
throughput = self.throughput_recent(recent_window)
|
|
319
|
+
if throughput is not None:
|
|
320
|
+
if throughput == 0:
|
|
321
|
+
if self.total is not None and self.position >= self.total:
|
|
322
|
+
return 'idle'
|
|
323
|
+
rightv.append(stalled)
|
|
324
|
+
else:
|
|
325
|
+
if throughput >= 10:
|
|
326
|
+
throughput = int(throughput)
|
|
327
|
+
rightv.append(self.format_counter(throughput, max_parts=1) + '/s')
|
|
328
|
+
remaining = self.remaining_time
|
|
329
|
+
if remaining:
|
|
330
|
+
remaining = int(remaining)
|
|
331
|
+
if remaining is not None:
|
|
332
|
+
rightv.append('ETA ' + transcribe_time(remaining))
|
|
333
|
+
if self.total is not None and self.total > 0:
|
|
334
|
+
leftv.append(self.text_pos_of_total())
|
|
335
|
+
else:
|
|
336
|
+
leftv.append(self.format_counter(self.position))
|
|
337
|
+
# the n/m display
|
|
338
|
+
left = ' '.join(leftv)
|
|
339
|
+
# the throughput display
|
|
340
|
+
right = ' '.join(rightv)
|
|
341
|
+
if self.total is None:
|
|
342
|
+
arrow_field = ' '
|
|
343
|
+
else:
|
|
344
|
+
# how much room for an arrow? we would like:
|
|
345
|
+
# "label: left arrow right"
|
|
346
|
+
arrow_width = width - len(left) - len(right) - 2
|
|
347
|
+
if label: # allow for ': ' separator after label
|
|
348
|
+
arrow_width -= len(label) + 2
|
|
349
|
+
if arrow_width < 3: # no room for an arrow
|
|
350
|
+
arrow_field = ':'
|
|
351
|
+
else:
|
|
352
|
+
arrow_field = ' ' + self.arrow(arrow_width) + ' '
|
|
353
|
+
status_line = left + arrow_field + right
|
|
354
|
+
if label:
|
|
355
|
+
label_width = width - len(status_line)
|
|
356
|
+
if label_width >= len(label) + 2:
|
|
357
|
+
prefix = label + ': '
|
|
358
|
+
elif label_width == len(label) + 1:
|
|
359
|
+
prefix = label + ':'
|
|
360
|
+
elif label_width <= 0: # label_width<=len(label): need to crop the label
|
|
361
|
+
# no room
|
|
362
|
+
prefix = ''
|
|
363
|
+
elif label_width == 1: # just indicate the crop
|
|
364
|
+
prefix = '<'
|
|
365
|
+
elif label_width == 2: # just indicate the crop
|
|
366
|
+
prefix = '<:'
|
|
367
|
+
else:
|
|
368
|
+
# crop as "<tail-of-label:"
|
|
369
|
+
prefix = '<' + label[-label_width + 2:] + ':'
|
|
370
|
+
else:
|
|
371
|
+
prefix = ''
|
|
372
|
+
status_line = prefix + status_line
|
|
373
|
+
return status_line
|
|
374
|
+
|
|
375
|
+
# pylint: disable=blacklisted-name,too-many-arguments
|
|
376
|
+
@contextmanager
|
|
377
|
+
@uses_quiet
|
|
378
|
+
@uses_upd
|
|
379
|
+
def bar(
|
|
380
|
+
self,
|
|
381
|
+
label=None,
|
|
382
|
+
*,
|
|
383
|
+
statusfunc=None,
|
|
384
|
+
width=None,
|
|
385
|
+
recent_window=None,
|
|
386
|
+
stalled=None,
|
|
387
|
+
report_print=None,
|
|
388
|
+
insert_pos=1,
|
|
389
|
+
poll: Optional[Callable[["BaseProgress"], None]] = None,
|
|
390
|
+
update_period=DEFAULT_UPDATE_PERIOD,
|
|
391
|
+
quiet: bool,
|
|
392
|
+
upd: Upd,
|
|
393
|
+
):
|
|
394
|
+
''' A context manager to create and withdraw a progress bar.
|
|
395
|
+
It returns the `UpdProxy` which displays the progress bar.
|
|
396
|
+
|
|
397
|
+
Parameters:
|
|
398
|
+
* `label`: a label for the progress bar,
|
|
399
|
+
default from `self.name`.
|
|
400
|
+
* `statusfunc`: an optional function to compute the progress bar text
|
|
401
|
+
accepting `(self,label,width)`.
|
|
402
|
+
* `width`: an optional width expressing how wide the progress bar
|
|
403
|
+
text may be.
|
|
404
|
+
The default comes from the `proxy.width` property.
|
|
405
|
+
* `recent_window`: optional timeframe to define "recent" in seconds;
|
|
406
|
+
if the default `statusfunc` (`Progress.status`) is used
|
|
407
|
+
this is passed to it
|
|
408
|
+
* `report_print`: optional `print` compatible function
|
|
409
|
+
with which to write a report on completion;
|
|
410
|
+
this may also be a `bool`, which if true will use `Upd.print`
|
|
411
|
+
in order to interoperate with `Upd`.
|
|
412
|
+
* `stalled`: optional string to replace the word `'stalled'`
|
|
413
|
+
in the status line; for a worked this might be betteer as `'idle'`
|
|
414
|
+
* `insert_pos`: where to insert the progress bar, default `1`
|
|
415
|
+
* `poll`: an optional callable accepting a `BaseProgress`
|
|
416
|
+
which can be used to update the progress state before
|
|
417
|
+
updating the progress bar display
|
|
418
|
+
|
|
419
|
+
Example use:
|
|
420
|
+
|
|
421
|
+
# display progress reporting during upload_filename()
|
|
422
|
+
# which updates the supplied Progress instance
|
|
423
|
+
# during its operation
|
|
424
|
+
P = Progress(name=label)
|
|
425
|
+
with P.bar(report_print=True):
|
|
426
|
+
upload_filename(src, progress=P)
|
|
427
|
+
|
|
428
|
+
'''
|
|
429
|
+
if label is None:
|
|
430
|
+
label = self.name
|
|
431
|
+
if report_print is None:
|
|
432
|
+
report_print = not quiet
|
|
433
|
+
if statusfunc is None:
|
|
434
|
+
|
|
435
|
+
def statusfunc(P, label, width):
|
|
436
|
+
''' Use the `Progress.status` method by default.
|
|
437
|
+
'''
|
|
438
|
+
return P.status(
|
|
439
|
+
label,
|
|
440
|
+
width,
|
|
441
|
+
recent_window=recent_window,
|
|
442
|
+
stalled=stalled,
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
def text_auto():
|
|
446
|
+
''' The current state of the `Progress`, to fit `width` and `proxy.width`.
|
|
447
|
+
'''
|
|
448
|
+
if poll is not None:
|
|
449
|
+
poll(self)
|
|
450
|
+
return statusfunc(self, "", min((width or proxy.width), proxy.width))
|
|
451
|
+
|
|
452
|
+
# pylint: disable=unused-argument
|
|
453
|
+
def update(P: Progress, _):
|
|
454
|
+
''' Update the status bar `UpdProxy` with the current state.
|
|
455
|
+
'''
|
|
456
|
+
proxy.text = None
|
|
457
|
+
|
|
458
|
+
cancel_ticker = False
|
|
459
|
+
|
|
460
|
+
def ticker():
|
|
461
|
+
''' Worker to update the progress bar every `update_period` seconds.
|
|
462
|
+
'''
|
|
463
|
+
time.sleep(update_period)
|
|
464
|
+
while not cancel_ticker:
|
|
465
|
+
update(self, None)
|
|
466
|
+
time.sleep(update_period)
|
|
467
|
+
|
|
468
|
+
if update_period == 0:
|
|
469
|
+
self.notify_update.add(update)
|
|
470
|
+
try:
|
|
471
|
+
start_pos = self.position
|
|
472
|
+
with upd.insert(
|
|
473
|
+
insert_pos,
|
|
474
|
+
prefix=label + ' ',
|
|
475
|
+
text_auto=text_auto,
|
|
476
|
+
) as proxy:
|
|
477
|
+
update(self, None)
|
|
478
|
+
if update_period > 0:
|
|
479
|
+
bg(ticker, daemon=True)
|
|
480
|
+
yield proxy
|
|
481
|
+
finally:
|
|
482
|
+
cancel_ticker = True
|
|
483
|
+
if update_period == 0:
|
|
484
|
+
self.notify_update.remove(update)
|
|
485
|
+
if report_print:
|
|
486
|
+
if isinstance(report_print, bool):
|
|
487
|
+
report_print = print
|
|
488
|
+
report_print(
|
|
489
|
+
label + ':', self.format_counter(self.position - start_pos), 'in',
|
|
490
|
+
transcribe_units(
|
|
491
|
+
self.elapsed_time, TIME_SCALE, max_parts=2, skip_zero=True
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
# pylint: disable=too-many-arguments,too-many-branches,too-many-locals
|
|
496
|
+
def iterbar(
|
|
497
|
+
self,
|
|
498
|
+
it,
|
|
499
|
+
label=None,
|
|
500
|
+
*,
|
|
501
|
+
itemlenfunc=None,
|
|
502
|
+
incfirst=False,
|
|
503
|
+
update_period=DEFAULT_UPDATE_PERIOD,
|
|
504
|
+
cancelled=None,
|
|
505
|
+
**bar_kw,
|
|
506
|
+
):
|
|
507
|
+
''' An iterable progress bar: a generator yielding values
|
|
508
|
+
from the iterable `it` while updating a progress bar.
|
|
509
|
+
|
|
510
|
+
Parameters:
|
|
511
|
+
* `it`: the iterable to consume and yield.
|
|
512
|
+
* `label`: a label for the progress bar,
|
|
513
|
+
default from `self.name`.
|
|
514
|
+
* `itemlenfunc`: an optional function returning the "size" of each item
|
|
515
|
+
from `it`, used to advance `self.position`.
|
|
516
|
+
The default is to assume a size of `1`.
|
|
517
|
+
A convenient alternative choice may be the builtin function `len`.
|
|
518
|
+
* `incfirst`: whether to advance `self.position` before we
|
|
519
|
+
`yield` an item from `it` or afterwards.
|
|
520
|
+
This reflects whether it is considered that progress is
|
|
521
|
+
made as items are obtained or only after items are processed
|
|
522
|
+
by whatever is consuming this generator.
|
|
523
|
+
The default is `False`, advancing after processing.
|
|
524
|
+
* `update_period`: default `DEFAULT_UPDATE_PERIOD`; if `0`
|
|
525
|
+
then update on every iteration, otherwise every `update_period`
|
|
526
|
+
seconds
|
|
527
|
+
* `cancelled`: an optional callable to test for iteration cancellation
|
|
528
|
+
Other parameters are passed to `Progress.bar`.
|
|
529
|
+
|
|
530
|
+
Example use:
|
|
531
|
+
|
|
532
|
+
from cs.units import DECIMAL_SCALE
|
|
533
|
+
rows = [some list of data]
|
|
534
|
+
P = Progress(total=len(rows), units_scale=DECIMAL_SCALE)
|
|
535
|
+
for row in P.iterbar(rows, incfirst=True):
|
|
536
|
+
... do something with each row ...
|
|
537
|
+
|
|
538
|
+
f = open(data_filename, 'rb')
|
|
539
|
+
datalen = os.stat(f).st_size
|
|
540
|
+
def readfrom(f):
|
|
541
|
+
while True:
|
|
542
|
+
bs = f.read(65536)
|
|
543
|
+
if not bs:
|
|
544
|
+
break
|
|
545
|
+
yield bs
|
|
546
|
+
P = Progress(total=datalen)
|
|
547
|
+
for bs in P.iterbar(readfrom(f), itemlenfunc=len):
|
|
548
|
+
... process the file data in bs ...
|
|
549
|
+
'''
|
|
550
|
+
with self.bar(label, update_period=update_period, **bar_kw) as proxy:
|
|
551
|
+
for item in it:
|
|
552
|
+
if cancelled and cancelled():
|
|
553
|
+
break
|
|
554
|
+
length = itemlenfunc(item) if itemlenfunc else 1
|
|
555
|
+
if incfirst:
|
|
556
|
+
self += length
|
|
557
|
+
if update_period == 0:
|
|
558
|
+
proxy.text = None
|
|
559
|
+
yield item
|
|
560
|
+
else:
|
|
561
|
+
yield item
|
|
562
|
+
self += length
|
|
563
|
+
if update_period == 0:
|
|
564
|
+
proxy.text = None
|
|
565
|
+
|
|
566
|
+
def qbar(self, label=None, **iterbar_kw) -> QueueIterator:
|
|
567
|
+
''' Set up a progress bar, return a `QueueIterator` for receiving items.
|
|
568
|
+
This is a shim for `Progress.iterbar` which dispatches a
|
|
569
|
+
worker to iterate a queue which received items placed on
|
|
570
|
+
the queue.
|
|
571
|
+
'''
|
|
572
|
+
Q = IterableQueue(name=label)
|
|
573
|
+
|
|
574
|
+
def qbar_worker():
|
|
575
|
+
for _ in self.iterbar(Q, label=label, **iterbar_kw):
|
|
576
|
+
pass
|
|
577
|
+
|
|
578
|
+
T = Thread(target=qbar_worker, name=f'{self}.qbar.qbar_worker:{label}')
|
|
579
|
+
T.start()
|
|
580
|
+
return Q
|
|
581
|
+
|
|
582
|
+
CheckPoint = namedtuple('CheckPoint', 'time position')
|
|
583
|
+
|
|
584
|
+
class Progress(BaseProgress):
|
|
585
|
+
''' A progress counter to track task completion with various utility methods.
|
|
586
|
+
|
|
587
|
+
Example:
|
|
588
|
+
|
|
589
|
+
>>> P = Progress(name="example")
|
|
590
|
+
>>> P #doctest: +ELLIPSIS
|
|
591
|
+
Progress(name='example',start=0,position=0,start_time=...,throughput_window=None,total=None):[CheckPoint(time=..., position=0)]
|
|
592
|
+
>>> P.advance(5)
|
|
593
|
+
>>> P #doctest: +ELLIPSIS
|
|
594
|
+
Progress(name='example',start=0,position=5,start_time=...,throughput_window=None,total=None):[CheckPoint(time=..., position=0), CheckPoint(time=..., position=5)]
|
|
595
|
+
>>> P.total = 100
|
|
596
|
+
>>> P #doctest: +ELLIPSIS
|
|
597
|
+
Progress(name='example',start=0,position=5,start_time=...,throughput_window=None,total=100):[CheckPoint(time=..., position=0), CheckPoint(time=..., position=5)]
|
|
598
|
+
|
|
599
|
+
A Progress instance has an attribute ``notify_update`` which
|
|
600
|
+
is a set of callables. Whenever the position is updated, each
|
|
601
|
+
of these will be called with the `Progress` instance and the
|
|
602
|
+
latest `CheckPoint`.
|
|
603
|
+
|
|
604
|
+
`Progress` objects also make a small pretense of being an integer.
|
|
605
|
+
The expression `int(progress)` returns the current position,
|
|
606
|
+
and `+=` and `-=` adjust the position.
|
|
607
|
+
|
|
608
|
+
This is convenient for coding, but importantly it is also
|
|
609
|
+
useful for discretionary use of a Progress with some other
|
|
610
|
+
object.
|
|
611
|
+
If you want to make a lightweight `Progress` capable class
|
|
612
|
+
you can set a position attribute to an `int`
|
|
613
|
+
and manipulate it carefully using `+=` and `-=` entirely.
|
|
614
|
+
If you decide to incur the cost of maintaining a `Progress` object
|
|
615
|
+
you can slot it in:
|
|
616
|
+
|
|
617
|
+
# initial setup with just an int
|
|
618
|
+
my_thing.amount = 0
|
|
619
|
+
|
|
620
|
+
# later, or on some option, use a Progress instance
|
|
621
|
+
my_thing.amount = Progress(my_thing.amount)
|
|
622
|
+
'''
|
|
623
|
+
|
|
624
|
+
# pylint: disable=too-many-arguments
|
|
625
|
+
@typechecked
|
|
626
|
+
def __init__(
|
|
627
|
+
self,
|
|
628
|
+
name: Optional[str] = None,
|
|
629
|
+
*,
|
|
630
|
+
position: Optional[float] = None,
|
|
631
|
+
start: Optional[float] = None,
|
|
632
|
+
start_time: Optional[float] = None,
|
|
633
|
+
throughput_window: Optional[int] = None,
|
|
634
|
+
total: Optional[float] = None,
|
|
635
|
+
units_scale=None,
|
|
636
|
+
):
|
|
637
|
+
''' Initialise the Progesss object.
|
|
638
|
+
|
|
639
|
+
Parameters:
|
|
640
|
+
* `position`: initial position, default `0`.
|
|
641
|
+
* `name`: optional name for this instance.
|
|
642
|
+
* `start`: starting position of progress range,
|
|
643
|
+
default from `position`.
|
|
644
|
+
* `start_time`: start time of the process, default now.
|
|
645
|
+
* `throughput_window`: length of throughput time window in seconds,
|
|
646
|
+
default None.
|
|
647
|
+
* `total`: expected completion value, default None.
|
|
648
|
+
'''
|
|
649
|
+
BaseProgress.__init__(
|
|
650
|
+
self, name=name, start_time=start_time, units_scale=units_scale
|
|
651
|
+
)
|
|
652
|
+
if position is None:
|
|
653
|
+
position = 0
|
|
654
|
+
if start is None:
|
|
655
|
+
start = position
|
|
656
|
+
if throughput_window is None:
|
|
657
|
+
throughput_window = DEFAULT_THROUGHPUT_WINDOW
|
|
658
|
+
elif throughput_window <= 0:
|
|
659
|
+
raise ValueError("throughput_window <= 0: %s" % (throughput_window,))
|
|
660
|
+
self.start = start
|
|
661
|
+
self._total = total
|
|
662
|
+
self.throughput_window = throughput_window
|
|
663
|
+
# history of positions, used to compute throughput
|
|
664
|
+
positions = [CheckPoint(self.start_time, start)]
|
|
665
|
+
if position != start:
|
|
666
|
+
positions.append(CheckPoint(time.time(), position))
|
|
667
|
+
self._positions = positions
|
|
668
|
+
self._flushed = True
|
|
669
|
+
|
|
670
|
+
def __repr__(self):
|
|
671
|
+
return "%s(name=%r,start=%s,position=%s,start_time=%s,throughput_window=%s,total=%s)" % (
|
|
672
|
+
type(self).__name__,
|
|
673
|
+
self.name,
|
|
674
|
+
self.start,
|
|
675
|
+
self.position,
|
|
676
|
+
self.start_time,
|
|
677
|
+
self.throughput_window,
|
|
678
|
+
self.total,
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
def _updated(self):
|
|
682
|
+
datum = self.latest
|
|
683
|
+
for notify in list(self.notify_update):
|
|
684
|
+
try:
|
|
685
|
+
notify(self, datum)
|
|
686
|
+
except Exception as e: # pylint: disable=broad-except
|
|
687
|
+
exception("%s: notify_update %s: %s", self, notify, e)
|
|
688
|
+
|
|
689
|
+
@property
|
|
690
|
+
def latest(self):
|
|
691
|
+
''' Latest datum.
|
|
692
|
+
'''
|
|
693
|
+
return self._positions[-1]
|
|
694
|
+
|
|
695
|
+
@property
|
|
696
|
+
def position(self):
|
|
697
|
+
''' Latest position.
|
|
698
|
+
'''
|
|
699
|
+
return self.latest.position
|
|
700
|
+
|
|
701
|
+
@position.setter
|
|
702
|
+
def position(self, new_position):
|
|
703
|
+
''' Update the latest position.
|
|
704
|
+
'''
|
|
705
|
+
self.update(new_position)
|
|
706
|
+
|
|
707
|
+
@property
|
|
708
|
+
def total(self):
|
|
709
|
+
''' Return the current total.
|
|
710
|
+
'''
|
|
711
|
+
return self._total
|
|
712
|
+
|
|
713
|
+
@total.setter
|
|
714
|
+
def total(self, new_total):
|
|
715
|
+
''' Update the total.
|
|
716
|
+
'''
|
|
717
|
+
self._total = new_total
|
|
718
|
+
self._updated()
|
|
719
|
+
|
|
720
|
+
def advance_total(self, delta):
|
|
721
|
+
''' Function form of addition to the total.
|
|
722
|
+
'''
|
|
723
|
+
self.total += delta
|
|
724
|
+
|
|
725
|
+
def update(self, new_position, update_time=None):
|
|
726
|
+
''' Record more progress.
|
|
727
|
+
|
|
728
|
+
>>> P = Progress()
|
|
729
|
+
>>> P.position
|
|
730
|
+
0
|
|
731
|
+
>>> P.update(12)
|
|
732
|
+
>>> P.position
|
|
733
|
+
12
|
|
734
|
+
'''
|
|
735
|
+
if new_position < self.latest.position:
|
|
736
|
+
debug(
|
|
737
|
+
"%s.update: new position %s < latest position %s", self,
|
|
738
|
+
new_position, self.latest.position
|
|
739
|
+
)
|
|
740
|
+
if update_time is None:
|
|
741
|
+
update_time = time.time()
|
|
742
|
+
datum = CheckPoint(update_time, new_position)
|
|
743
|
+
self._positions.append(datum)
|
|
744
|
+
self._flushed = False
|
|
745
|
+
self._updated()
|
|
746
|
+
|
|
747
|
+
def advance(self, delta, update_time=None):
|
|
748
|
+
''' Record more progress, return the advanced position.
|
|
749
|
+
|
|
750
|
+
>>> P = Progress()
|
|
751
|
+
>>> P.position
|
|
752
|
+
0
|
|
753
|
+
>>> P.advance(4)
|
|
754
|
+
>>> P.position
|
|
755
|
+
4
|
|
756
|
+
>>> P.advance(4)
|
|
757
|
+
>>> P.position
|
|
758
|
+
8
|
|
759
|
+
'''
|
|
760
|
+
self.update(self.position + delta, update_time=update_time)
|
|
761
|
+
|
|
762
|
+
def __iadd__(self, delta):
|
|
763
|
+
''' Operator += form of advance().
|
|
764
|
+
|
|
765
|
+
>>> P = Progress()
|
|
766
|
+
>>> P.position
|
|
767
|
+
0
|
|
768
|
+
>>> P += 4
|
|
769
|
+
>>> P.position
|
|
770
|
+
4
|
|
771
|
+
>>> P += 4
|
|
772
|
+
>>> P.position
|
|
773
|
+
8
|
|
774
|
+
'''
|
|
775
|
+
self.advance(delta)
|
|
776
|
+
return self
|
|
777
|
+
|
|
778
|
+
def __isub__(self, delta):
|
|
779
|
+
''' Operator -= form of advance().
|
|
780
|
+
|
|
781
|
+
>>> P = Progress()
|
|
782
|
+
>>> P.position
|
|
783
|
+
0
|
|
784
|
+
>>> P += 4
|
|
785
|
+
>>> P.position
|
|
786
|
+
4
|
|
787
|
+
>>> P -= 4
|
|
788
|
+
>>> P.position
|
|
789
|
+
0
|
|
790
|
+
'''
|
|
791
|
+
self.advance(-delta)
|
|
792
|
+
return self
|
|
793
|
+
|
|
794
|
+
def _flush(self, oldest=None):
|
|
795
|
+
if oldest is None:
|
|
796
|
+
window = self.throughput_window
|
|
797
|
+
if window is None:
|
|
798
|
+
raise ValueError(
|
|
799
|
+
"oldest may not be None when throughput_window is None"
|
|
800
|
+
)
|
|
801
|
+
oldest = time.time() - window
|
|
802
|
+
positions = self._positions
|
|
803
|
+
# scan for first item still in time window,
|
|
804
|
+
# never discard the last 2 positions
|
|
805
|
+
for ndx in range(0, len(positions) - 1):
|
|
806
|
+
posn = positions[ndx]
|
|
807
|
+
if posn.time >= oldest:
|
|
808
|
+
# this is the first element to keep, discard preceeding (if any)
|
|
809
|
+
# note we can't just start at ndx=1 because ndx=0 might be in range
|
|
810
|
+
del positions[0:ndx]
|
|
811
|
+
break
|
|
812
|
+
self._flushed = True
|
|
813
|
+
|
|
814
|
+
@property
|
|
815
|
+
def throughput(self):
|
|
816
|
+
''' Current throughput per second.
|
|
817
|
+
|
|
818
|
+
If `self.throughput_window` is not `None`,
|
|
819
|
+
calls `self.throughput_recent(throughput_window)`.
|
|
820
|
+
Otherwise call `self.throughput_overall()`.
|
|
821
|
+
'''
|
|
822
|
+
throughput_window = self.throughput_window
|
|
823
|
+
if throughput_window is None:
|
|
824
|
+
return self.throughput_overall()
|
|
825
|
+
return self.throughput_recent(throughput_window)
|
|
826
|
+
|
|
827
|
+
def throughput_recent(self, time_window):
|
|
828
|
+
''' Recent throughput per second within a time window in seconds.
|
|
829
|
+
|
|
830
|
+
The time span overlapping the start of the window is included
|
|
831
|
+
on a flat pro rata basis.
|
|
832
|
+
'''
|
|
833
|
+
if time_window <= 0:
|
|
834
|
+
raise ValueError(
|
|
835
|
+
"%s.throughput_recent: invalid time_window <= 0: %s" %
|
|
836
|
+
(self, time_window)
|
|
837
|
+
)
|
|
838
|
+
if not self._flushed:
|
|
839
|
+
self._flush()
|
|
840
|
+
positions = self._positions
|
|
841
|
+
if len(positions) == 1 and positions[0].time == self.start_time:
|
|
842
|
+
# no throughput if we only have the starting position
|
|
843
|
+
return None
|
|
844
|
+
now = time.time()
|
|
845
|
+
time0 = now - time_window
|
|
846
|
+
if time0 < self.start_time:
|
|
847
|
+
time0 = self.start_time
|
|
848
|
+
# lowest time and position
|
|
849
|
+
# low_time will be time0
|
|
850
|
+
# low_pos will be the matching position, probably interpolated
|
|
851
|
+
low_time = None
|
|
852
|
+
low_pos = None
|
|
853
|
+
# walk forward through the samples, assumes monotonic
|
|
854
|
+
for t, p in self._positions:
|
|
855
|
+
if t >= time0:
|
|
856
|
+
low_time = t
|
|
857
|
+
low_pos = p
|
|
858
|
+
break
|
|
859
|
+
if low_time is None:
|
|
860
|
+
# no samples within the window; caller might infer stall
|
|
861
|
+
return 0
|
|
862
|
+
if low_time >= now:
|
|
863
|
+
# in the future? warn and return 0
|
|
864
|
+
debug('low_time=%s >= now=%s', low_time, now)
|
|
865
|
+
return 0
|
|
866
|
+
rate = float(self.position - low_pos) / (now - low_time)
|
|
867
|
+
if rate < 0:
|
|
868
|
+
debug('rate < 0 (%s)', rate)
|
|
869
|
+
return rate
|
|
870
|
+
|
|
871
|
+
class OverProgress(BaseProgress):
|
|
872
|
+
''' A `Progress`-like class computed from a set of subsidiary `Progress`es.
|
|
873
|
+
|
|
874
|
+
AN OverProgress instance has an attribute ``notify_update`` which
|
|
875
|
+
is a set of callables.
|
|
876
|
+
Whenever the position of a subsidiary `Progress` is updated,
|
|
877
|
+
each of these will be called with the `Progress` instance and `None`.
|
|
878
|
+
|
|
879
|
+
Example:
|
|
880
|
+
|
|
881
|
+
>>> P = OverProgress(name="over")
|
|
882
|
+
>>> P1 = Progress(name="progress1", position=12)
|
|
883
|
+
>>> P1.total = 100
|
|
884
|
+
>>> P1.advance(7)
|
|
885
|
+
>>> P2 = Progress(name="progress2", position=20)
|
|
886
|
+
>>> P2.total = 50
|
|
887
|
+
>>> P2.advance(9)
|
|
888
|
+
>>> P.add(P1)
|
|
889
|
+
>>> P.add(P2)
|
|
890
|
+
>>> P1.total
|
|
891
|
+
100
|
|
892
|
+
>>> P2.total
|
|
893
|
+
50
|
|
894
|
+
>>> P.total
|
|
895
|
+
150
|
|
896
|
+
>>> P1.start
|
|
897
|
+
12
|
|
898
|
+
>>> P2.start
|
|
899
|
+
20
|
|
900
|
+
>>> P.start
|
|
901
|
+
0
|
|
902
|
+
>>> P1.position
|
|
903
|
+
19
|
|
904
|
+
>>> P2.position
|
|
905
|
+
29
|
|
906
|
+
>>> P.position
|
|
907
|
+
16
|
|
908
|
+
|
|
909
|
+
'''
|
|
910
|
+
|
|
911
|
+
def __init__(
|
|
912
|
+
self, subprogresses=None, name=None, start_time=None, units_scale=None
|
|
913
|
+
):
|
|
914
|
+
BaseProgress.__init__(
|
|
915
|
+
self, name=name, start_time=start_time, units_scale=units_scale
|
|
916
|
+
)
|
|
917
|
+
# we use these to to accrue removed subprogresses (optional)
|
|
918
|
+
self._base_total = 0
|
|
919
|
+
self._base_position = 0
|
|
920
|
+
self.subprogresses = set()
|
|
921
|
+
if subprogresses:
|
|
922
|
+
for subP in subprogresses:
|
|
923
|
+
self.add(subP)
|
|
924
|
+
|
|
925
|
+
def __repr__(self):
|
|
926
|
+
return "%s(name=%r,start=%s,position=%s,start_time=%s,total=%s)" \
|
|
927
|
+
% (
|
|
928
|
+
type(self).__name__, self.name,
|
|
929
|
+
self.start, self.position, self.start_time,
|
|
930
|
+
self.total)
|
|
931
|
+
|
|
932
|
+
def _updated(self):
|
|
933
|
+
with self._lock:
|
|
934
|
+
notifiers = list(self.notify_update)
|
|
935
|
+
for notify in notifiers:
|
|
936
|
+
try:
|
|
937
|
+
notify(self, None)
|
|
938
|
+
except Exception as e: # pylint: disable=broad-except
|
|
939
|
+
exception("%s: notify_update %s: %s", self, notify, e)
|
|
940
|
+
|
|
941
|
+
# pylint: disable=unused-argument
|
|
942
|
+
def _child_updated(self, child, _):
|
|
943
|
+
''' Notify watchers if a child updates.
|
|
944
|
+
'''
|
|
945
|
+
self._updated()
|
|
946
|
+
|
|
947
|
+
def add(self, subprogress):
|
|
948
|
+
''' Add a subsidairy `Progress` to the contributing set.
|
|
949
|
+
'''
|
|
950
|
+
with self._lock:
|
|
951
|
+
subprogress.notify_update.add(self._child_updated)
|
|
952
|
+
self.subprogresses.add(subprogress)
|
|
953
|
+
self._updated()
|
|
954
|
+
|
|
955
|
+
def remove(self, subprogress, accrue=False):
|
|
956
|
+
''' Remove a subsidairy `Progress` from the contributing set.
|
|
957
|
+
'''
|
|
958
|
+
with self._lock:
|
|
959
|
+
subprogress.notify_update.remove(self._child_updated)
|
|
960
|
+
self.subprogresses.remove(subprogress)
|
|
961
|
+
if accrue:
|
|
962
|
+
self._base_position += subprogress.position - subprogress.start
|
|
963
|
+
self._base_total += subprogress.total
|
|
964
|
+
self._updated()
|
|
965
|
+
|
|
966
|
+
@property
|
|
967
|
+
def start(self):
|
|
968
|
+
''' We always return a starting value of 0.
|
|
969
|
+
'''
|
|
970
|
+
return 0
|
|
971
|
+
|
|
972
|
+
def _overmax(self, fnP):
|
|
973
|
+
''' Return the maximum of the non-`None` values
|
|
974
|
+
computed from the subsidiary `Progress`es.
|
|
975
|
+
Return the maximum, or `None` if there are no non-`None` values.
|
|
976
|
+
'''
|
|
977
|
+
with self._lock:
|
|
978
|
+
children = list(self.subprogresses)
|
|
979
|
+
maximum = None
|
|
980
|
+
for value in filter(fnP, children):
|
|
981
|
+
if value is not None:
|
|
982
|
+
maximum = value if maximum is None else max(maximum, value)
|
|
983
|
+
return maximum
|
|
984
|
+
|
|
985
|
+
def _oversum(self, fnP):
|
|
986
|
+
''' Sum non-`None` values computed from the subsidiary `Progress`es.
|
|
987
|
+
Return the sum, or `None` if there are no non-`None` values.
|
|
988
|
+
'''
|
|
989
|
+
with self._lock:
|
|
990
|
+
children = list(self.subprogresses)
|
|
991
|
+
summed = None
|
|
992
|
+
for value in map(fnP, children):
|
|
993
|
+
if value is not None:
|
|
994
|
+
summed = value if summed is None else summed + value
|
|
995
|
+
return summed
|
|
996
|
+
|
|
997
|
+
@property
|
|
998
|
+
def position(self):
|
|
999
|
+
''' The `position` is the sum off the subsidiary position offsets
|
|
1000
|
+
from their respective starts.
|
|
1001
|
+
'''
|
|
1002
|
+
pos = self._oversum(lambda P: P.position - P.start)
|
|
1003
|
+
if pos is None:
|
|
1004
|
+
pos = 0
|
|
1005
|
+
return self._base_position + pos
|
|
1006
|
+
|
|
1007
|
+
@property
|
|
1008
|
+
def total(self):
|
|
1009
|
+
''' The `total` is the sum of the subsidiary totals.
|
|
1010
|
+
'''
|
|
1011
|
+
total = self._oversum(lambda P: P.total)
|
|
1012
|
+
if total is None:
|
|
1013
|
+
total = 0
|
|
1014
|
+
return self._base_total + total
|
|
1015
|
+
|
|
1016
|
+
@property
|
|
1017
|
+
def throughput(self):
|
|
1018
|
+
''' The `throughput` is the sum of the subsidiary throughputs.
|
|
1019
|
+
'''
|
|
1020
|
+
return self._oversum(lambda P: P.throughput)
|
|
1021
|
+
|
|
1022
|
+
def throughput_recent(self, time_window):
|
|
1023
|
+
''' The `throughput_recent` is the sum of the subsidiary throughput_recentss.
|
|
1024
|
+
'''
|
|
1025
|
+
return self._oversum(lambda P: P.throughput_recent(time_window))
|
|
1026
|
+
|
|
1027
|
+
@property
|
|
1028
|
+
def eta(self):
|
|
1029
|
+
''' The `eta` is the maximum of the subsidiary etas.
|
|
1030
|
+
'''
|
|
1031
|
+
return self._overmax(lambda P: P.eta)
|
|
1032
|
+
|
|
1033
|
+
@uses_upd
|
|
1034
|
+
def progressbar(
|
|
1035
|
+
it,
|
|
1036
|
+
label=None,
|
|
1037
|
+
*,
|
|
1038
|
+
position=None,
|
|
1039
|
+
total=None,
|
|
1040
|
+
units_scale=UNSCALED_SCALE,
|
|
1041
|
+
upd: Upd,
|
|
1042
|
+
**iterbar_kw
|
|
1043
|
+
):
|
|
1044
|
+
''' Convenience function to construct and run a `Progress.iterbar`
|
|
1045
|
+
wrapping the iterable `it`,
|
|
1046
|
+
issuing and withdrawing a progress bar during the iteration.
|
|
1047
|
+
|
|
1048
|
+
Parameters:
|
|
1049
|
+
* `it`: the iterable to consume
|
|
1050
|
+
* `label`: optional label, doubles as the `Progress.name`
|
|
1051
|
+
* `position`: optional starting position
|
|
1052
|
+
* `total`: optional value for `Progress.total`,
|
|
1053
|
+
default from `len(it)` if supported.
|
|
1054
|
+
* `units_scale`: optional units scale for `Progress`,
|
|
1055
|
+
default `UNSCALED_SCALE`
|
|
1056
|
+
|
|
1057
|
+
If `total` is `None` and `it` supports `len()`
|
|
1058
|
+
then the `Progress.total` is set from it.
|
|
1059
|
+
|
|
1060
|
+
All arguments are passed through to `Progress.iterbar`.
|
|
1061
|
+
|
|
1062
|
+
Example use:
|
|
1063
|
+
|
|
1064
|
+
for row in progressbar(rows):
|
|
1065
|
+
... do something with row ...
|
|
1066
|
+
'''
|
|
1067
|
+
if upd is None or upd.disabled:
|
|
1068
|
+
return it
|
|
1069
|
+
if total is None:
|
|
1070
|
+
try:
|
|
1071
|
+
total = len(it)
|
|
1072
|
+
except TypeError:
|
|
1073
|
+
total = None
|
|
1074
|
+
yield from Progress(
|
|
1075
|
+
name=label, position=position, total=total, units_scale=units_scale
|
|
1076
|
+
).iterbar(it, **iterbar_kw)
|
|
1077
|
+
|
|
1078
|
+
@decorator
|
|
1079
|
+
def auto_progressbar(func, label=None, report_print=False):
|
|
1080
|
+
''' Decorator for a function accepting an optional `progress`
|
|
1081
|
+
keyword parameter.
|
|
1082
|
+
If `progress` is not `None` and the default `Upd` is not disabled,
|
|
1083
|
+
run the function with a progress bar.
|
|
1084
|
+
'''
|
|
1085
|
+
|
|
1086
|
+
def wrapper(
|
|
1087
|
+
*a,
|
|
1088
|
+
progress=None,
|
|
1089
|
+
progress_name=None,
|
|
1090
|
+
progress_total=None,
|
|
1091
|
+
progress_report_print=None,
|
|
1092
|
+
**kw
|
|
1093
|
+
):
|
|
1094
|
+
if progress_name is None:
|
|
1095
|
+
progress_name = label or funcname(func)
|
|
1096
|
+
if progress_report_print is None:
|
|
1097
|
+
progress_report_print = report_print
|
|
1098
|
+
if progress is None:
|
|
1099
|
+
upd = Upd()
|
|
1100
|
+
if not upd.disabled:
|
|
1101
|
+
progress = Progress(name=progress_name, total=progress_total)
|
|
1102
|
+
with progress.bar(upd=upd, report_print=progress_report_print):
|
|
1103
|
+
return func(*a, progress=progress, **kw)
|
|
1104
|
+
return func(*a, progress=progress, **kw)
|
|
1105
|
+
|
|
1106
|
+
return wrapper
|
|
1107
|
+
|
|
1108
|
+
# pylint: disable=unused-argument
|
|
1109
|
+
def selftest(argv):
|
|
1110
|
+
''' Exercise some of the functionality.
|
|
1111
|
+
'''
|
|
1112
|
+
with open(__file__, encoding='utf8') as f:
|
|
1113
|
+
lines = f.readlines()
|
|
1114
|
+
lines += lines
|
|
1115
|
+
if True: # pylint: disable=using-constant-test
|
|
1116
|
+
for _ in progressbar(lines, "lines"):
|
|
1117
|
+
pass
|
|
1118
|
+
if True: # pylint: disable=using-constant-test
|
|
1119
|
+
for _ in progressbar(
|
|
1120
|
+
lines,
|
|
1121
|
+
"blines",
|
|
1122
|
+
units_scale=BINARY_BYTES_SCALE,
|
|
1123
|
+
itemlenfunc=len,
|
|
1124
|
+
total=sum(len(line) for line in lines),
|
|
1125
|
+
):
|
|
1126
|
+
pass
|
|
1127
|
+
if True: # pylint: disable=using-constant-test
|
|
1128
|
+
for _ in progressbar(
|
|
1129
|
+
lines,
|
|
1130
|
+
"lines update 2s",
|
|
1131
|
+
update_period=2,
|
|
1132
|
+
report_print=True,
|
|
1133
|
+
):
|
|
1134
|
+
pass
|
|
1135
|
+
if True: # pylint: disable=using-constant-test
|
|
1136
|
+
P = Progress(
|
|
1137
|
+
name=__file__,
|
|
1138
|
+
##total=len(lines),
|
|
1139
|
+
units_scale=DECIMAL_SCALE,
|
|
1140
|
+
)
|
|
1141
|
+
with open(__file__, encoding='utf8') as f:
|
|
1142
|
+
for _ in P.iterbar(f):
|
|
1143
|
+
time.sleep(0.005)
|
|
1144
|
+
from cs.debug import selftest as runtests # pylint: disable=import-outside-toplevel
|
|
1145
|
+
runtests('cs.progress_tests')
|
|
1146
|
+
|
|
1147
|
+
if __name__ == '__main__':
|
|
1148
|
+
sys.exit(selftest(sys.argv))
|