dcicutils 8.8.3.1b11__py3-none-any.whl → 8.8.3.1b13__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/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,,