dcicutils 8.8.3.1b4__py3-none-any.whl → 8.8.3.1b7__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/misc_utils.py CHANGED
@@ -2548,6 +2548,29 @@ def normalize_spaces(value: str) -> str:
2548
2548
  return re.sub(r"\s+", " ", value).strip()
2549
2549
 
2550
2550
 
2551
+ def find_nth_from_end(string: str, substring: str, nth: int) -> int:
2552
+ """
2553
+ Returns the index of the nth occurrence of the given substring within
2554
+ the given string from the END of the given string; or -1 if not found.
2555
+ """
2556
+ index = -1
2557
+ string = string[::-1]
2558
+ for i in range(0, nth):
2559
+ index = string.find(substring, index + 1)
2560
+ return len(string) - index - 1 if index >= 0 else -1
2561
+
2562
+
2563
+ def set_nth(string: str, nth: int, replacement: str) -> str:
2564
+ """
2565
+ Sets the nth character of the given string to the given replacement string.
2566
+ """
2567
+ if not isinstance(string, str) or not isinstance(nth, int) or not isinstance(replacement, str):
2568
+ return string
2569
+ if nth < 0:
2570
+ nth += len(string)
2571
+ return string[:nth] + replacement + string[nth + 1:] if 0 <= nth < len(string) else string
2572
+
2573
+
2551
2574
  class JsonLinesReader:
2552
2575
 
2553
2576
  def __init__(self, fp, padded=False, padding=None):
dcicutils/progress_bar.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from collections import namedtuple
2
+ import re
2
3
  from signal import signal, SIGINT
3
4
  import sys
4
5
  import threading
@@ -8,6 +9,7 @@ from types import FrameType as frame
8
9
  from typing import Callable, List, Optional, Union
9
10
  from contextlib import contextmanager
10
11
  from dcicutils.command_utils import yes_or_no
12
+ from dcicutils.misc_utils import find_nth_from_end, set_nth
11
13
 
12
14
 
13
15
  class TQDM(tqdm):
@@ -59,6 +61,8 @@ class ProgressBar:
59
61
  self._bar = None
60
62
  self._disabled = False
61
63
  self._done = False
64
+ self._really_done = False
65
+ self.foo = time.time()
62
66
  self._tidy_output_hack = (tidy_output_hack is True)
63
67
  self._started = time.time()
64
68
  self._stop_requested = False
@@ -102,11 +106,9 @@ class ProgressBar:
102
106
  return True
103
107
  return False
104
108
 
105
- def set_total(self, value: int, reset_eta: bool = False) -> None:
109
+ def set_total(self, value: int) -> None:
106
110
  if value == self._total:
107
111
  # If the total has not changed since last set then do nothing.
108
- if reset_eta and self._bar is not None:
109
- self._bar.reset()
110
112
  return
111
113
  if isinstance(value, int) and value > 0:
112
114
  self._total = value
@@ -117,16 +119,6 @@ class ProgressBar:
117
119
  self._bar.total = value
118
120
  self._bar.refresh()
119
121
 
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
- progress = self._bar.n
125
- self._bar.reset()
126
- self._bar.total = self._total
127
- self._bar.n = progress
128
- self._bar.refresh()
129
-
130
122
  def set_progress(self, value: int) -> None:
131
123
  if isinstance(value, int) and value >= 0:
132
124
  if (self._bar is not None) or self._initialize():
@@ -139,17 +131,29 @@ class ProgressBar:
139
131
  self._bar.update(value)
140
132
  self._bar.refresh()
141
133
 
142
- def set_description(self, value: str) -> None:
143
- self._description = self._format_description(value)
134
+ def reset_eta(self) -> None:
135
+ # Since set_total does nothing if total is the same, provide
136
+ # a way to reset the ETA if starting over with the same total.
137
+ # But NOTE that resetting ETA will ALSO reset the ELAPSED time.
144
138
  if self._bar is not None:
145
- self._bar.set_description(self._description)
139
+ progress = self._bar.n
140
+ self._bar.reset()
141
+ self._bar.total = self._total
142
+ self._bar.n = progress
143
+ self._bar.refresh()
144
+
145
+ def set_description(self, value: str) -> None:
146
+ if isinstance(value, str):
147
+ self._description = self._format_description(value)
148
+ if self._bar is not None:
149
+ self._bar.set_description(self._description)
146
150
 
147
- def done(self) -> None:
151
+ def done(self, description: Optional[str] = None) -> None:
148
152
  if self._done or self._bar is None:
149
153
  return
150
154
  self._ended = time.time()
151
155
  self.set_progress(self.total)
152
- self._bar.set_description(self._description)
156
+ self.set_description(description)
153
157
  self._bar.refresh()
154
158
  # FYI: Do NOT do a bar.disable = True before a bar.close() or it messes up output
155
159
  # on multiple calls; found out the hard way; a couple hours will never get back :-/
@@ -200,6 +204,12 @@ class ProgressBar:
200
204
  def captured_output_for_testing(self) -> Optional[List[str]]:
201
205
  return self._captured_output_for_testing
202
206
 
207
+ @staticmethod
208
+ def format_captured_output_for_testing(description: str, total: int, progress: int) -> str:
209
+ percent = round((progress / total) * 100.0)
210
+ separator = "✓" if percent == 100 else "|"
211
+ return f"{description} {separator} {percent:>3}% ◀|### | {progress}/{total} | 0.0/s | 00:00 | ETA: 00:00"
212
+
203
213
  def _format_description(self, value: str) -> str:
204
214
  if not isinstance(value, str):
205
215
  value = ""
@@ -259,29 +269,26 @@ class ProgressBar:
259
269
  # string in the display string where the progress bar should actually go,
260
270
  # which we do in _format_description. Other minor things too; see below.
261
271
  sys_stdout_write = sys.stdout.write
262
- last_total = None ; last_progress = None ; last_text = None # noqa
272
+ last_text = None ; last_captured_output_text = None # noqa
263
273
  def tidy_stdout_write(text: str) -> None: # noqa
264
274
  nonlocal self, sys_stdout_write, sentinel_internal, spina, spini, spinn
265
- nonlocal last_total, last_progress, last_text
275
+ nonlocal last_text, last_captured_output_text
266
276
  def replace_first(value: str, match: str, replacement: str) -> str: # noqa
267
277
  return value[:i] + replacement + value[i + len(match):] if (i := value.find(match)) >= 0 else value
268
278
  def remove_extra_trailing_spaces(text: str) -> str: # noqa
269
279
  while text.endswith(" "):
270
280
  text = text[:-1]
271
281
  return text
272
- if not text:
282
+ if (not text) or (last_text == text) or self._really_done:
273
283
  return
274
- if self._bar:
275
- if ((self._bar.total == last_total) and (self._bar.n == last_progress) and (last_text == text)):
276
- return
277
- last_total = self._bar.total ; last_progress = self._bar.n ; last_text = text # noqa
284
+ last_text = text
278
285
  if (self._disabled or self._done) and sentinel_internal in text:
279
286
  # Another hack to really disable output on interrupt; in this case we set
280
287
  # tqdm.disable to True, but output can still dribble out, so if the output
281
288
  # looks like it is from tqdm and we are disabled/done then do no output.
282
289
  return
283
290
  if sentinel_internal in text:
284
- spinc = spina[spini % spinn] if not ("100%|" in text) else "| ✓" ; spini += 1 # noqa
291
+ spinc = spina[spini % spinn] if not ("100%|" in text) else "✓" ; spini += 1 # noqa
285
292
  text = replace_first(text, sentinel_internal, f" {spinc}")
286
293
  text = replace_first(text, "%|", "% ◀|")
287
294
  text = remove_extra_trailing_spaces(text)
@@ -290,9 +297,33 @@ class ProgressBar:
290
297
  # the unit we gave, which is empty; idunno; just replace it here.
291
298
  text = replace_first(text, "s/ ", "/s ")
292
299
  sys_stdout_write(text)
293
- if self._captured_output_for_testing is not None:
294
- self._captured_output_for_testing.append(text)
295
300
  sys.stdout.flush()
301
+ if self._done:
302
+ self._really_done = True
303
+ return
304
+ if self._captured_output_for_testing is not None:
305
+ # For testing only we replace vacilliting values in the out like rate,
306
+ # time elapsed, and ETA with static values; so that something like this:
307
+ # > Working / 20% ◀|█████████▌ | 1/5 | 536.00/s | 00:01 | ETA: 00:02
308
+ # becomes something more static like this after calling this function:
309
+ # > Working | 20% ◀|### | 1/5 | 0.0/s | 00:00 | ETA: 00:00
310
+ # This function obviously has intimate knowledge of the output; better here than in tests.
311
+ def replace_time_dependent_values_with_static(text: str) -> str:
312
+ blocks = "\u2587|\u2588|\u2589|\u258a|\u258b|\u258c|\u258d|\u258e|\u258f"
313
+ if (n := find_nth_from_end(text, "|", 5)) >= 8:
314
+ pattern = re.compile(
315
+ rf"(\s*)(\d*%? ◀\|)(?:\s*{blocks}|#)*\s*(\|\s*\d+/\d+)?(\s*\|\s*)"
316
+ rf"(?:\d+\.?\d*|\?)(\/s\s*\|\s*)(?:\d+:\d+)?(\s*\|\s*ETA:\s*)(?:\d+:\d+|\?)?")
317
+ if match := pattern.match(text[n - 6:]):
318
+ if text[n - 8:n - 7] != "✓": text = set_nth(text, n - 8, "|") # noqa
319
+ return (text[0:n - 6].replace("\r", "") +
320
+ match.expand(rf"\g<1>\g<2>### \g<3>\g<4>0.0\g<5>00:00\g<6>00:00"))
321
+ return text
322
+ if text != "\n":
323
+ captured_output_text = replace_time_dependent_values_with_static(text)
324
+ if captured_output_text != last_captured_output_text:
325
+ self._captured_output_for_testing.append(captured_output_text)
326
+ last_captured_output_text = captured_output_text
296
327
  def restore_stdout_write() -> None: # noqa
297
328
  nonlocal sys_stdout_write
298
329
  if sys_stdout_write is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcicutils
3
- Version: 8.8.3.1b4
3
+ Version: 8.8.3.1b7
4
4
  Summary: Utility package for interacting with the 4DN Data Portal and other 4DN resources
5
5
  Home-page: https://github.com/4dn-dcic/utils
6
6
  License: MIT
@@ -43,12 +43,12 @@ dcicutils/license_policies/park-lab-gpl-pipeline.jsonc,sha256=vLZkwm3Js-kjV44nug
43
43
  dcicutils/license_policies/park-lab-pipeline.jsonc,sha256=9qlY0ASy3iUMQlr3gorVcXrSfRHnVGbLhkS427UaRy4,283
44
44
  dcicutils/license_utils.py,sha256=d1cq6iwv5Ju-VjdoINi6q7CPNNL7Oz6rcJdLMY38RX0,46978
45
45
  dcicutils/log_utils.py,sha256=7pWMc6vyrorUZQf-V-M3YC6zrPgNhuV_fzm9xqTPph0,10883
46
- dcicutils/misc_utils.py,sha256=zVc4urdVGgnWjQ4UQlrGH-URAzr2l_PwZWI3u_GJdFE,102210
46
+ dcicutils/misc_utils.py,sha256=a_grjJdiYgEMctwnXy7uDoFtDvCfAv1gjrLxdrOkptM,103041
47
47
  dcicutils/obfuscation_utils.py,sha256=fo2jOmDRC6xWpYX49u80bVNisqRRoPskFNX3ymFAmjw,5963
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=Xm0IqL2dA9C2gx98cPEbvlo81V76bEmpUpxb_8S3VqM,30480
51
- dcicutils/progress_bar.py,sha256=ZuWJJ7FAkv8ndU1vwcg-0TAG76Tm05m3-d97G4b7YzE,14463
51
+ dcicutils/progress_bar.py,sha256=j6dZdh5EeZo52GWIBu2JL5254-V6eOhKHHB2eCkgevI,16569
52
52
  dcicutils/project_utils.py,sha256=qPdCaFmWUVBJw4rw342iUytwdQC0P-XKpK4mhyIulMM,31250
53
53
  dcicutils/qa_checkers.py,sha256=cdXjeL0jCDFDLT8VR8Px78aS10hwNISOO5G_Zv2TZ6M,20534
54
54
  dcicutils/qa_utils.py,sha256=TT0SiJWiuxYvbsIyhK9VO4uV_suxhB6CpuC4qPacCzQ,160208
@@ -72,8 +72,8 @@ dcicutils/trace_utils.py,sha256=g8kwV4ebEy5kXW6oOrEAUsurBcCROvwtZqz9fczsGRE,1769
72
72
  dcicutils/validation_utils.py,sha256=cMZIU2cY98FYtzK52z5WUYck7urH6JcqOuz9jkXpqzg,14797
73
73
  dcicutils/variant_utils.py,sha256=2H9azNx3xAj-MySg-uZ2SFqbWs4kZvf61JnK6b-h4Qw,4343
74
74
  dcicutils/zip_utils.py,sha256=rnjNv_k6L9jT2SjDSgVXp4BEJYLtz9XN6Cl2Fy-tqnM,2027
75
- dcicutils-8.8.3.1b4.dist-info/LICENSE.txt,sha256=qnwSmfnEWMl5l78VPDEzAmEbLVrRqQvfUQiHT0ehrOo,1102
76
- dcicutils-8.8.3.1b4.dist-info/METADATA,sha256=SU3-GNJsxb2pu-dBBV8wXLAigORCxsI0-oYH6uE2gdc,3356
77
- dcicutils-8.8.3.1b4.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
78
- dcicutils-8.8.3.1b4.dist-info/entry_points.txt,sha256=51Q4F_2V10L0282W7HFjP4jdzW4K8lnWDARJQVFy_hw,270
79
- dcicutils-8.8.3.1b4.dist-info/RECORD,,
75
+ dcicutils-8.8.3.1b7.dist-info/LICENSE.txt,sha256=qnwSmfnEWMl5l78VPDEzAmEbLVrRqQvfUQiHT0ehrOo,1102
76
+ dcicutils-8.8.3.1b7.dist-info/METADATA,sha256=v8fZzOT3EWHNMI-R_bT-jbA1LTjKnGd1LOGD_oLn1Yg,3356
77
+ dcicutils-8.8.3.1b7.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
78
+ dcicutils-8.8.3.1b7.dist-info/entry_points.txt,sha256=51Q4F_2V10L0282W7HFjP4jdzW4K8lnWDARJQVFy_hw,270
79
+ dcicutils-8.8.3.1b7.dist-info/RECORD,,