dcicutils 8.8.3.1b11__py3-none-any.whl → 8.8.3.1b13__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
dcicutils/data_readers.py CHANGED
@@ -66,6 +66,13 @@ class RowReader(abc.ABC):
66
66
  else:
67
67
  return value
68
68
 
69
+ @property
70
+ def nrows(self) -> int:
71
+ nrows = 0
72
+ for row in self:
73
+ nrows += 1
74
+ return nrows
75
+
69
76
  def open(self) -> None:
70
77
  pass
71
78
 
@@ -192,6 +199,10 @@ class Excel:
192
199
  return True
193
200
  return False
194
201
 
202
+ @property
203
+ def nsheets(self) -> int:
204
+ return len(self.sheet_names)
205
+
195
206
  def __del__(self) -> None:
196
207
  if (workbook := self._workbook) is not None:
197
208
  self._workbook = None
@@ -1,6 +1,7 @@
1
1
  from dcicutils.misc_utils import normalize_spaces
2
2
  from datetime import datetime, timedelta, timezone
3
- from typing import Optional, Tuple
3
+ from dateutil import parser as datetime_parser
4
+ from typing import Optional, Tuple, Union
4
5
 
5
6
 
6
7
  def parse_datetime_string(value: str) -> Optional[datetime]:
@@ -82,6 +83,32 @@ def normalize_date_string(value: str) -> Optional[str]:
82
83
  return d.strftime("%Y-%m-%d") if d else None
83
84
 
84
85
 
86
+ def get_timezone(hours: int, minutes: Optional[int] = None) -> timezone:
87
+ try:
88
+ return timezone(timedelta(hours=hours, minutes=minutes or 0))
89
+ except Exception:
90
+ return timezone.utc
91
+
92
+
93
+ def get_timezone_hours_minutes(tz: timezone) -> Tuple[int, int]:
94
+ """
95
+ Returns a tuple with the integer hours and minutes offset for the given timezone.
96
+ """
97
+ tz_minutes = datetime.now(tz).utcoffset().total_seconds() / 60
98
+ return int(tz_minutes // 60), int(abs(tz_minutes % 60))
99
+
100
+
101
+ def get_utc_timezone() -> timezone:
102
+ return timezone.utc
103
+
104
+
105
+ def get_local_timezone() -> timezone:
106
+ """
107
+ Returns current/local timezone as a datetime.timezone object.
108
+ """
109
+ return datetime.now().astimezone().tzinfo
110
+
111
+
85
112
  def get_local_timezone_string() -> str:
86
113
  """
87
114
  Returns current/local timezone in format like: "-05:00".
@@ -96,3 +123,85 @@ def get_local_timezone_hours_minutes() -> Tuple[int, int]:
96
123
  """
97
124
  tz_minutes = datetime.now(timezone.utc).astimezone().utcoffset().total_seconds() / 60
98
125
  return int(tz_minutes // 60), int(abs(tz_minutes % 60))
126
+
127
+
128
+ def parse_datetime(value: str, utc: bool = False, tz: Optional[timezone] = None) -> Optional[datetime]:
129
+ """
130
+ Parses the given string into a datetime, if possible, and returns that value,
131
+ or None if not able to parse. The timezone of the returned datetime will be the
132
+ local timezone; or if the given utc argument is True then it will be UTC; or if the
133
+ given tz argument is a datetime.timezone then return datetime will be in that timezone.
134
+ """
135
+ if isinstance(value, datetime):
136
+ return value
137
+ elif not isinstance(value, str):
138
+ return None
139
+ try:
140
+ # This dateutil.parser handles quite a wide variety of formats and suits our needs.
141
+ value = datetime_parser.parse(value)
142
+ if utc is True:
143
+ # If the given utc argument is True then it trumps any tz argument if given.
144
+ tz = timezone.utc
145
+ if value.tzinfo is not None:
146
+ # The given value had an explicit timezone specified.
147
+ if isinstance(tz, timezone):
148
+ return value.astimezone(tz)
149
+ return value
150
+ return value.replace(tzinfo=tz if isinstance(tz, timezone) else get_local_timezone())
151
+ except Exception:
152
+ return None
153
+
154
+
155
+ def format_datetime(value: datetime,
156
+ utc: bool = False,
157
+ iso: bool = False,
158
+ ms: bool = False,
159
+ tz: Optional[Union[timezone, bool]] = None,
160
+ notz: bool = False,
161
+ noseconds: bool = False,
162
+ verbose: bool = False,
163
+ noseparator: bool = False,
164
+ noday: bool = False) -> Optional[str]:
165
+ """
166
+ Returns the given datetime as a string in "YYYY:MM:DD hh:mm:ss tz" format, for
167
+ example "2024-04-17 15:42:26 EDT". If the given notz argument is True then omits
168
+ the timezone; if the noseconds argument is given the omits the seconds. If the given
169
+ verbose argument is True then returns a really verbose version of the datetime, for
170
+ example "Wednesday, April 17, 2024 | 15:42:26 EDT"; if the noseparator argument is
171
+ True then omits the "|" separator; if the noday argument is True then omits the day
172
+ of week part. The timezone of the returned datetime string will default to the local
173
+ one; if the given utc argument is True then it will be UTC; or if the given tz
174
+ argument is a datetime.timezone it will be in that timezone.
175
+ """
176
+ if not isinstance(value, datetime):
177
+ if not isinstance(value, str) or not (value := parse_datetime(value)):
178
+ return None
179
+ try:
180
+ if utc is True:
181
+ tz = timezone.utc
182
+ elif not isinstance(tz, timezone):
183
+ tz = get_local_timezone()
184
+ if tz is True:
185
+ notz = False
186
+ elif tz is False:
187
+ notz = True
188
+ value = value.astimezone(tz)
189
+ if iso:
190
+ if notz is True:
191
+ value = value.replace(tzinfo=None)
192
+ if not (ms is True):
193
+ value = value.replace(microsecond=0)
194
+ return value.isoformat()
195
+ if noseconds is True:
196
+ ms = False
197
+ if verbose:
198
+ return value.strftime(
199
+ f"{'' if noday is True else '%A, '}%B %-d, %Y{'' if noseparator is True else ' |'}"
200
+ f" %-I:%M{'' if noseconds is True else ':%S'}"
201
+ f"{f'.%f' if ms is True else ''} %p{'' if notz is True else ' %Z'}")
202
+ else:
203
+ return value.strftime(
204
+ f"%Y-%m-%d %H:%M{'' if noseconds is True else ':%S'}"
205
+ f"{f'.%f' if ms is True else ''}{'' if notz is True else ' %Z'}")
206
+ except Exception:
207
+ return None
dcicutils/misc_utils.py CHANGED
@@ -2571,6 +2571,48 @@ def set_nth(string: str, nth: int, replacement: str) -> str:
2571
2571
  return string[:nth] + replacement + string[nth + 1:] if 0 <= nth < len(string) else string
2572
2572
 
2573
2573
 
2574
+ def format_size(nbytes: Union[int, float], precision: int = 2, nospace: bool = False, terse: bool = False) -> str:
2575
+ if isinstance(nbytes, str) and nbytes.isdigit():
2576
+ nbytes = int(nbytes)
2577
+ elif not isinstance(nbytes, (int, float)):
2578
+ return ""
2579
+ UNITS = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
2580
+ UNITS_TERSE = ['b', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
2581
+ MAX_UNITS_INDEX = len(UNITS) - 1
2582
+ ONE_K = 1024
2583
+ index = 0
2584
+ if (precision := max(precision, 0)) and (nbytes <= ONE_K):
2585
+ precision -= 1
2586
+ while abs(nbytes) >= ONE_K and index < MAX_UNITS_INDEX:
2587
+ nbytes /= ONE_K
2588
+ index += 1
2589
+ if index == 0:
2590
+ nbytes = int(nbytes)
2591
+ return f"{nbytes} byte{'s' if nbytes != 1 else ''}"
2592
+ unit = (UNITS_TERSE if terse else UNITS)[index]
2593
+ return f"{nbytes:.{precision}f}{'' if nospace else ' '}{unit}"
2594
+
2595
+
2596
+ def format_duration(seconds: Union[int, float]) -> str:
2597
+ seconds_actual = seconds
2598
+ seconds = round(max(seconds, 0))
2599
+ durations = [("year", 31536000), ("day", 86400), ("hour", 3600), ("minute", 60), ("second", 1)]
2600
+ parts = []
2601
+ for name, duration in durations:
2602
+ if seconds >= duration:
2603
+ count = seconds // duration
2604
+ seconds %= duration
2605
+ if count != 1:
2606
+ name += "s"
2607
+ parts.append(f"{count} {name}")
2608
+ if len(parts) == 0:
2609
+ return f"{seconds_actual:.1f} seconds"
2610
+ elif len(parts) == 1:
2611
+ return f"{seconds_actual:.1f} seconds"
2612
+ else:
2613
+ return " ".join(parts[:-1]) + " " + parts[-1]
2614
+
2615
+
2574
2616
  class JsonLinesReader:
2575
2617
 
2576
2618
  def __init__(self, fp, padded=False, padding=None):
dcicutils/portal_utils.py CHANGED
@@ -282,15 +282,17 @@ class Portal:
282
282
  except Exception:
283
283
  return None
284
284
 
285
- def patch_metadata(self, object_id: str, data: dict) -> Optional[dict]:
285
+ def patch_metadata(self, object_id: str, data: dict, check_only: bool = False) -> Optional[dict]:
286
286
  if self.key:
287
- return patch_metadata(obj_id=object_id, patch_item=data, key=self.key)
288
- return self.patch(f"/{object_id}", data).json()
287
+ return patch_metadata(obj_id=object_id, patch_item=data, key=self.key,
288
+ add_on="check_only=True" if check_only else "")
289
+ return self.patch(f"/{object_id}{'?check_only=True' if check_only else ''}", data).json()
289
290
 
290
- def post_metadata(self, object_type: str, data: dict) -> Optional[dict]:
291
+ def post_metadata(self, object_type: str, data: dict, check_only: bool = False) -> Optional[dict]:
291
292
  if self.key:
292
- return post_metadata(schema_name=object_type, post_item=data, key=self.key)
293
- return self.post(f"/{object_type}", data).json()
293
+ return post_metadata(schema_name=object_type, post_item=data, key=self.key,
294
+ add_on="check_only=True" if check_only else "")
295
+ return self.post(f"/{object_type}{'?check_only=True' if check_only else ''}", data).json()
294
296
 
295
297
  def get_health(self) -> OptionalResponse:
296
298
  return self.get("/health")
dcicutils/progress_bar.py CHANGED
@@ -9,7 +9,7 @@ from types import FrameType as frame
9
9
  from typing import Callable, List, Optional, Union
10
10
  from contextlib import contextmanager
11
11
  from dcicutils.command_utils import yes_or_no
12
- from dcicutils.misc_utils import find_nth_from_end, set_nth
12
+ from dcicutils.misc_utils import find_nth_from_end, format_size, set_nth
13
13
 
14
14
 
15
15
  class TQDM(tqdm):
@@ -49,6 +49,8 @@ class ProgressBar:
49
49
 
50
50
  def __init__(self, total: Optional[int] = None,
51
51
  description: Optional[str] = None,
52
+ use_byte_size_for_rate: bool = False,
53
+ use_ascii: bool = False,
52
54
  catch_interrupt: bool = True,
53
55
  interrupt: Optional[Callable] = None,
54
56
  interrupt_continue: Optional[Callable] = None,
@@ -59,11 +61,13 @@ class ProgressBar:
59
61
  tidy_output_hack: bool = True,
60
62
  capture_output_for_testing: bool = False) -> None:
61
63
  self._bar = None
64
+ self._started = 0
62
65
  self._disabled = False
63
66
  self._done = False
64
67
  self._tidy_output_hack = (tidy_output_hack is True)
65
- self._started = time.time()
66
68
  self._stop_requested = False
69
+ self._use_byte_size_for_rate = (use_byte_size_for_rate is True and self._tidy_output_hack)
70
+ self._use_ascii = (use_ascii is True)
67
71
  # Interrupt handling. We do not do the actual (signal) interrupt setup
68
72
  # in self._initialize as that could be called from a (sub) thread; and in
69
73
  # Python we can only set a signal (SIGINT in our case) on the main thread.
@@ -96,9 +100,13 @@ class ProgressBar:
96
100
  def _initialize(self) -> bool:
97
101
  # Do not actually create the tqdm object unless/until we have a positive total.
98
102
  if (self._bar is None) and (self._total > 0):
99
- bar_format = "{l_bar}{bar}| {n_fmt}/{total_fmt} | {rate_fmt} | {elapsed}{postfix} | ETA: {remaining} "
103
+ if self._use_byte_size_for_rate:
104
+ bar_format = "{l_bar}{bar}| {n_fmt}/{total_fmt} | [rate] | {elapsed}{postfix} | ETA: {remaining} "
105
+ else:
106
+ bar_format = "{l_bar}{bar}| {n_fmt}/{total_fmt} | {rate_fmt} | {elapsed}{postfix} | ETA: {remaining} "
100
107
  self._bar = TQDM(total=self._total, desc=self._description,
101
- dynamic_ncols=True, bar_format=bar_format, unit="", file=sys.stdout)
108
+ dynamic_ncols=True, bar_format=bar_format, unit="", file=sys.stdout, ascii=self._use_ascii)
109
+ self._started = time.time()
102
110
  if self._disabled:
103
111
  self._bar.disable = True
104
112
  return True
@@ -153,6 +161,7 @@ class ProgressBar:
153
161
  self.set_total(total, _norefresh=True)
154
162
  self.set_progress(progress, _norefresh=True)
155
163
  self.set_description(description)
164
+ self._started = time.time()
156
165
 
157
166
  def done(self, description: Optional[str] = None) -> None:
158
167
  if self._done or self._bar is None:
@@ -198,14 +207,6 @@ class ProgressBar:
198
207
  def stop_requested(self) -> bool:
199
208
  return self._stop_requested
200
209
 
201
- @property
202
- def started(self) -> None:
203
- return self._started
204
-
205
- @property
206
- def duration(self) -> None:
207
- return time.time() - self._started
208
-
209
210
  @property
210
211
  def captured_output_for_testing(self) -> Optional[List[str]]:
211
212
  return self._captured_output_for_testing
@@ -302,6 +303,9 @@ class ProgressBar:
302
303
  # something like "1.54s/" rather than "1.54/s"; something to do with
303
304
  # the unit we gave, which is empty; idunno; just replace it here.
304
305
  text = replace_first(text, "s/ ", "/s ")
306
+ if self._use_byte_size_for_rate and self._bar:
307
+ rate = self._bar.n / (time.time() - self._started)
308
+ text = text.replace("[rate]", f"{format_size(rate)}/s")
305
309
  sys_stdout_write(text)
306
310
  sys.stdout.flush()
307
311
  if self._captured_output_for_testing is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcicutils
3
- Version: 8.8.3.1b11
3
+ Version: 8.8.3.1b13
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
@@ -10,9 +10,9 @@ dcicutils/common.py,sha256=YE8Mt5-vaZWWz4uaChSVhqGFbFtW5QKtnIyOr4zG4vM,3955
10
10
  dcicutils/contribution_scripts.py,sha256=0k5Gw1TumcD5SAcXVkDd6-yvuMEw-jUp5Kfb7FJH6XQ,2015
11
11
  dcicutils/contribution_utils.py,sha256=vYLS1JUB3sKd24BUxZ29qUBqYeQBLK9cwo8x3k64uPg,25653
12
12
  dcicutils/creds_utils.py,sha256=xrLekD49Ex0GOpL9n7LlJA4gvNcY7txTVFOSYD7LvEU,11113
13
- dcicutils/data_readers.py,sha256=WWH_VDz2KnNv_FoTjfFwrg6zh9asl8Q-uEV2V3XuyUg,7414
13
+ dcicutils/data_readers.py,sha256=6EMrY7TjDE8H7bA_TCWtpLQP7slJ0YTL77_dNh6e7sg,7626
14
14
  dcicutils/data_utils.py,sha256=k2OxOlsx7AJ6jF-YNlMyGus_JqSUBe4_n1s65Mv1gQQ,3098
15
- dcicutils/datetime_utils.py,sha256=EODDGAngp1yh2ZlDIuI7tB74JBJucw2DljqfPknzK0Y,4666
15
+ dcicutils/datetime_utils.py,sha256=OXdNpOKjNL3osTwhmLoJeL3DXoMksp0egExDT1hif_Y,9152
16
16
  dcicutils/deployment_utils.py,sha256=sKv8Jb-_Zw2aH3OAywRlsre-Cqm3a7fEGG3_1PT-r-s,69908
17
17
  dcicutils/diff_utils.py,sha256=sQx-yz56DHAcQWOChYbAG3clXu7TbiZKlw-GggeveO0,8118
18
18
  dcicutils/docker_utils.py,sha256=30gUiqz7X9rJwSPXTPn4ewjQibUgoSJqhP9o9vn5X-A,1747
@@ -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=a_grjJdiYgEMctwnXy7uDoFtDvCfAv1gjrLxdrOkptM,103041
46
+ dcicutils/misc_utils.py,sha256=YH_TTmv6ABWeMERwVvA2-rIfdS-CoPYLXJru9TvWxgM,104610
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
- dcicutils/portal_utils.py,sha256=Xm0IqL2dA9C2gx98cPEbvlo81V76bEmpUpxb_8S3VqM,30480
51
- dcicutils/progress_bar.py,sha256=OFgLXJL-03tQw2WM6JVDr8_YhxN3eay090iUhiAJKZ4,16830
50
+ dcicutils/portal_utils.py,sha256=DYyE5o15GekDgzpJWas9iS7klAYbjJZUPW0G42McArk,30779
51
+ dcicutils/progress_bar.py,sha256=th-M7Q1UfkmQWc8M36lfVoRhFAQVRT9fSpdHi5RHqNY,17389
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.1b11.dist-info/LICENSE.txt,sha256=qnwSmfnEWMl5l78VPDEzAmEbLVrRqQvfUQiHT0ehrOo,1102
76
- dcicutils-8.8.3.1b11.dist-info/METADATA,sha256=X_eKy99Z5pjG-7rAnp-JC1sFIRJscq29KBx-J-CV-n0,3357
77
- dcicutils-8.8.3.1b11.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
78
- dcicutils-8.8.3.1b11.dist-info/entry_points.txt,sha256=51Q4F_2V10L0282W7HFjP4jdzW4K8lnWDARJQVFy_hw,270
79
- dcicutils-8.8.3.1b11.dist-info/RECORD,,
75
+ dcicutils-8.8.3.1b13.dist-info/LICENSE.txt,sha256=qnwSmfnEWMl5l78VPDEzAmEbLVrRqQvfUQiHT0ehrOo,1102
76
+ dcicutils-8.8.3.1b13.dist-info/METADATA,sha256=3mHRqaCku3ZV_Yea9_Na_QZ8LTCscx9sELeZtt1BaUc,3357
77
+ dcicutils-8.8.3.1b13.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
78
+ dcicutils-8.8.3.1b13.dist-info/entry_points.txt,sha256=51Q4F_2V10L0282W7HFjP4jdzW4K8lnWDARJQVFy_hw,270
79
+ dcicutils-8.8.3.1b13.dist-info/RECORD,,