anemoi-utils 0.3.12__py3-none-any.whl → 0.3.14__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.

Potentially problematic release.


This version of anemoi-utils might be problematic. Click here for more details.

anemoi/utils/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.3.12'
16
- __version_tuple__ = version_tuple = (0, 3, 12)
15
+ __version__ = version = '0.3.14'
16
+ __version_tuple__ = version_tuple = (0, 3, 14)
anemoi/utils/cli.py CHANGED
@@ -12,6 +12,11 @@ import os
12
12
  import sys
13
13
  import traceback
14
14
 
15
+ try:
16
+ import argcomplete
17
+ except ImportError:
18
+ argcomplete = None
19
+
15
20
  LOG = logging.getLogger(__name__)
16
21
 
17
22
 
@@ -100,6 +105,8 @@ def register_commands(here, package, select, fail=None):
100
105
  def cli_main(version, description, commands):
101
106
  parser = make_parser(description, commands)
102
107
  args, unknown = parser.parse_known_args()
108
+ if argcomplete:
109
+ argcomplete.autocomplete(parser)
103
110
 
104
111
  if args.version:
105
112
  print(version)
anemoi/utils/dates.py CHANGED
@@ -8,6 +8,9 @@
8
8
 
9
9
  import calendar
10
10
  import datetime
11
+ import re
12
+
13
+ import isodate
11
14
 
12
15
  from .hindcasts import HindcastDatesTimes
13
16
 
@@ -66,6 +69,122 @@ def as_datetime(date):
66
69
  raise ValueError(f"Invalid date type: {type(date)}")
67
70
 
68
71
 
72
+ def _compress_dates(dates):
73
+ dates = sorted(dates)
74
+ if len(dates) < 3:
75
+ yield dates
76
+ return
77
+
78
+ prev = first = dates.pop(0)
79
+ curr = dates.pop(0)
80
+ delta = curr - prev
81
+ while curr - prev == delta:
82
+ prev = curr
83
+ if not dates:
84
+ break
85
+ curr = dates.pop(0)
86
+
87
+ yield (first, prev, delta)
88
+ if dates:
89
+ yield from _compress_dates([curr] + dates)
90
+
91
+
92
+ def compress_dates(dates):
93
+ dates = [as_datetime(_) for _ in dates]
94
+ result = []
95
+
96
+ for n in _compress_dates(dates):
97
+ if isinstance(n, list):
98
+ result.extend([str(_) for _ in n])
99
+ else:
100
+ result.append(" ".join([str(n[0]), "to", str(n[1]), "by", str(n[2])]))
101
+
102
+ return result
103
+
104
+
105
+ def print_dates(dates):
106
+ print(compress_dates(dates))
107
+
108
+
109
+ def frequency_to_string(frequency):
110
+ # TODO: use iso8601
111
+ frequency = frequency_to_timedelta(frequency)
112
+
113
+ total_seconds = frequency.total_seconds()
114
+ assert int(total_seconds) == total_seconds, total_seconds
115
+ total_seconds = int(total_seconds)
116
+
117
+ seconds = total_seconds
118
+
119
+ days = seconds // (24 * 3600)
120
+ seconds %= 24 * 3600
121
+ hours = seconds // 3600
122
+ seconds %= 3600
123
+ minutes = seconds // 60
124
+ seconds %= 60
125
+
126
+ if days > 0 and hours == 0 and minutes == 0 and seconds == 0:
127
+ return f"{days}d"
128
+
129
+ if days == 0 and hours > 0 and minutes == 0 and seconds == 0:
130
+ return f"{hours}h"
131
+
132
+ if days == 0 and hours == 0 and minutes > 0 and seconds == 0:
133
+ return f"{minutes}m"
134
+
135
+ if days == 0 and hours == 0 and minutes == 0 and seconds > 0:
136
+ return f"{seconds}s"
137
+
138
+ if days > 0:
139
+ return f"{total_seconds}s"
140
+
141
+ return str(frequency)
142
+
143
+
144
+ def frequency_to_timedelta(frequency):
145
+ # TODO: use iso8601 or check pytimeparse
146
+
147
+ if isinstance(frequency, datetime.timedelta):
148
+ return frequency
149
+
150
+ if isinstance(frequency, int):
151
+ return datetime.timedelta(hours=frequency)
152
+
153
+ assert isinstance(frequency, str), (type(frequency), frequency)
154
+
155
+ try:
156
+ return frequency_to_timedelta(int(frequency))
157
+ except ValueError:
158
+ pass
159
+
160
+ if re.match(r"^\d+[hdms]$", frequency, re.IGNORECASE):
161
+ unit = frequency[-1].lower()
162
+ v = int(frequency[:-1])
163
+ unit = {"h": "hours", "d": "days", "s": "seconds", "m": "minutes"}[unit]
164
+ return datetime.timedelta(**{unit: v})
165
+
166
+ m = frequency.split(":")
167
+ if len(m) == 2:
168
+ return datetime.timedelta(hours=int(m[0]), minutes=int(m[1]))
169
+
170
+ if len(m) == 3:
171
+ return datetime.timedelta(hours=int(m[0]), minutes=int(m[1]), seconds=int(m[2]))
172
+
173
+ # ISO8601
174
+ try:
175
+ return isodate.parse_duration(frequency)
176
+ except isodate.isoerror.ISO8601Error:
177
+ pass
178
+
179
+ raise ValueError(f"Cannot convert frequency {frequency} to timedelta")
180
+
181
+
182
+ def normalize_date(x):
183
+ if isinstance(x, str):
184
+ return no_time_zone(datetime.datetime.fromisoformat(x))
185
+ return x
186
+
187
+
69
188
  DOW = {
70
189
  "monday": 0,
71
190
  "tuesday": 1,
anemoi/utils/humanize.py CHANGED
@@ -104,7 +104,7 @@ def _plural(count):
104
104
  def seconds_to_human(seconds: float) -> str:
105
105
  """Convert a number of seconds to a human readable string
106
106
 
107
- >>> seconds(4000)
107
+ >>> seconds_to_human(4000)
108
108
  '1 hour 6 minutes 40 seconds'
109
109
 
110
110
  Parameters
anemoi/utils/s3.py CHANGED
@@ -268,7 +268,7 @@ class Download(Transfer):
268
268
  def source_size(self, s3_object):
269
269
  return s3_object["Size"]
270
270
 
271
- def transfer_file(self, source, target, overwrite, resume, verbosity, progress, config=None):
271
+ def transfer_file(self, source, target, overwrite, resume, verbosity, progress=None, config=None):
272
272
  try:
273
273
  return self._transfer_file(source, target, overwrite, resume, verbosity, config=config)
274
274
  except Exception as e:
@@ -343,6 +343,7 @@ def upload(source, target, *, overwrite=False, resume=False, verbosity=1, progre
343
343
  """
344
344
 
345
345
  uploader = Upload()
346
+
346
347
  if os.path.isdir(source):
347
348
  uploader.transfer_folder(
348
349
  source=source,
anemoi/utils/text.py CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  """Text utilities"""
9
9
 
10
+ import re
10
11
  from collections import defaultdict
11
12
 
12
13
  # https://en.wikipedia.org/wiki/Box-drawing_character
@@ -32,6 +33,62 @@ def dotted_line(width=84) -> str:
32
33
  return "┈" * width
33
34
 
34
35
 
36
+ # Regular expression to match ANSI escape codes
37
+ _ansi_escape = re.compile(r"\x1b\[([0-9;]*[mGKH])")
38
+
39
+
40
+ def _has_ansi_escape(s):
41
+ return _ansi_escape.search(s) is not None
42
+
43
+
44
+ def _split_tokens(s):
45
+ """Split a string into a list of visual characters with their lenghts."""
46
+ from wcwidth import wcswidth
47
+
48
+ initial = s
49
+ out = []
50
+
51
+ # Function to probe the number of bytes needed to encode the first character
52
+ def probe_utf8(s):
53
+ for i in range(1, 5):
54
+ try:
55
+ s[:i].encode("utf-8")
56
+ except UnicodeEncodeError:
57
+ return i - 1
58
+ return 1
59
+
60
+ while s:
61
+ match = _ansi_escape.match(s)
62
+ if match:
63
+ token = match.group(0)
64
+ s = s[len(token) :]
65
+ out.append((token, 0))
66
+ else:
67
+ i = probe_utf8(s)
68
+ token = s[:i]
69
+ s = s[i:]
70
+ out.append((token, wcswidth(token)))
71
+
72
+ assert "".join(token for (token, _) in out) == initial, (out, initial)
73
+ return out
74
+
75
+
76
+ def visual_len(s):
77
+ """Compute the length of a string as it appears on the terminal."""
78
+ if isinstance(s, str):
79
+ s = _split_tokens(s)
80
+ assert isinstance(s, (tuple, list)), (type(s), s)
81
+ if len(s) == 0:
82
+ return 0
83
+ for _ in s:
84
+ assert isinstance(_, tuple), s
85
+ assert len(_) == 2, s
86
+ n = 0
87
+ for _, width in s:
88
+ n += width
89
+ return n
90
+
91
+
35
92
  def boxed(text, min_width=80, max_width=80) -> str:
36
93
  """Put a box around a text
37
94
 
@@ -57,27 +114,44 @@ def boxed(text, min_width=80, max_width=80) -> str:
57
114
 
58
115
  """
59
116
 
60
- lines = text.split("\n")
61
- width = max(len(_) for _ in lines)
117
+ lines = []
118
+ for line in text.split("\n"):
119
+ line = line.strip()
120
+ line = _split_tokens(line)
121
+ lines.append(line)
122
+
123
+ width = max(visual_len(_) for _ in lines)
62
124
 
63
125
  if min_width is not None:
64
126
  width = max(width, min_width)
65
127
 
66
128
  if max_width is not None:
129
+
130
+ def shorten_line(line, max_width):
131
+ if visual_len(line) > max_width:
132
+ while visual_len(line) >= max_width:
133
+ line = line[:-1]
134
+ line.append(("…", 1))
135
+ return line
136
+
67
137
  width = min(width, max_width)
68
- lines = []
69
- for line in text.split("\n"):
70
- if len(line) > max_width:
71
- line = line[: max_width - 1] + "…"
72
- lines.append(line)
73
- text = "\n".join(lines)
138
+ lines = [shorten_line(line, max_width) for line in lines]
139
+
140
+ def pad_line(line, width):
141
+ line = line + [" "] * (width - visual_len(line))
142
+ return line
143
+
144
+ lines = [pad_line(line, width) for line in lines]
74
145
 
75
146
  box = []
76
147
  box.append("┌" + "─" * (width + 2) + "┐")
77
148
  for line in lines:
78
- box.append(f"│ {line:{width}} │")
79
-
149
+ s = "".join(_[0] for _ in line)
150
+ if _has_ansi_escape(s):
151
+ s += "\x1b[0m"
152
+ box.append(f"│ {s} │")
80
153
  box.append("└" + "─" * (width + 2) + "┘")
154
+
81
155
  return "\n".join(box)
82
156
 
83
157
 
@@ -241,7 +315,7 @@ def table(rows, header, align, margin=0):
241
315
  ['B', 120, 1],
242
316
  ['C', 9, 123]],
243
317
  ['C1', 'C2', 'C3'],
244
- ['<', '>', '>']))
318
+ ['<', '>', '>'])
245
319
  C1 │ C2 │ C3
246
320
  ───┼─────┼────
247
321
  Aa │ 12 │ 5
anemoi/utils/timer.py CHANGED
@@ -10,7 +10,7 @@
10
10
  import logging
11
11
  import time
12
12
 
13
- from .humanize import seconds
13
+ from .humanize import seconds_to_human
14
14
 
15
15
  LOGGER = logging.getLogger(__name__)
16
16
 
@@ -31,4 +31,4 @@ class Timer:
31
31
  return time.time() - self.start
32
32
 
33
33
  def __exit__(self, *args):
34
- self.logger.info("%s: %s.", self.title, seconds(self.elapsed))
34
+ self.logger.info("%s: %s.", self.title, seconds_to_human(self.elapsed))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: anemoi-utils
3
- Version: 0.3.12
3
+ Version: 0.3.14
4
4
  Summary: A package to hold various functions to support training of ML models on ECMWF data.
5
5
  Author-email: "European Centre for Medium-Range Weather Forecasts (ECMWF)" <software.support@ecmwf.int>
6
6
  License: Apache License
@@ -223,6 +223,7 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
223
223
  Classifier: Programming Language :: Python :: Implementation :: PyPy
224
224
  Requires-Python: >=3.9
225
225
  License-File: LICENSE
226
+ Requires-Dist: isodate
226
227
  Requires-Dist: pyyaml
227
228
  Requires-Dist: tomli
228
229
  Requires-Dist: tqdm
@@ -239,19 +240,17 @@ Requires-Dist: pandoc ; extra == 'dev'
239
240
  Requires-Dist: pytest ; extra == 'dev'
240
241
  Requires-Dist: requests ; extra == 'dev'
241
242
  Requires-Dist: sphinx ; extra == 'dev'
242
- Requires-Dist: sphinx-argparse ; extra == 'dev'
243
+ Requires-Dist: sphinx-argparse <0.5 ; extra == 'dev'
243
244
  Requires-Dist: sphinx-rtd-theme ; extra == 'dev'
244
245
  Requires-Dist: termcolor ; extra == 'dev'
245
- Requires-Dist: tomli ; extra == 'dev'
246
246
  Provides-Extra: docs
247
247
  Requires-Dist: nbsphinx ; extra == 'docs'
248
248
  Requires-Dist: pandoc ; extra == 'docs'
249
249
  Requires-Dist: requests ; extra == 'docs'
250
250
  Requires-Dist: sphinx ; extra == 'docs'
251
- Requires-Dist: sphinx-argparse ; extra == 'docs'
251
+ Requires-Dist: sphinx-argparse <0.5 ; extra == 'docs'
252
252
  Requires-Dist: sphinx-rtd-theme ; extra == 'docs'
253
253
  Requires-Dist: termcolor ; extra == 'docs'
254
- Requires-Dist: tomli ; extra == 'docs'
255
254
  Provides-Extra: grib
256
255
  Requires-Dist: requests ; extra == 'grib'
257
256
  Provides-Extra: provenance
@@ -1,25 +1,25 @@
1
1
  anemoi/utils/__init__.py,sha256=zZZpbKIoGWwdCOuo6YSruLR7C0GzvzI1Wzhyqaa0K7M,456
2
2
  anemoi/utils/__main__.py,sha256=cLA2PidDTOUHaDGzd0_E5iioKYNe-PSTv567Y2fuwQk,723
3
- anemoi/utils/_version.py,sha256=KKAn9DIKrkf9v6mAp59Mf9qI6ZEjCmBhMjuc_1u5SFI,413
3
+ anemoi/utils/_version.py,sha256=uXvaQV0x-j_stHaI7VjSOLUNVrd36A2OO0ftk9b0Dmw,413
4
4
  anemoi/utils/caching.py,sha256=EZ4bRG72aTvTxzrbYCgjFpdIn8OtA1rzoRmGg8caWsI,1919
5
5
  anemoi/utils/checkpoints.py,sha256=1_3mg4B-ykTVfIvIUEv7IxGyREx_ZcilVbB3U-V6O6I,5165
6
- anemoi/utils/cli.py,sha256=4uMy6LyL9VPg9ZVNKsimahT5BBVPQgWWk7LUel5dQZE,3545
6
+ anemoi/utils/cli.py,sha256=SWb5_itARlDCq6yEf-VvagTioSW2phKXXFMW2ihXu18,3678
7
7
  anemoi/utils/config.py,sha256=s8eqlHsuak058_NJXGMOoT2HenwiZJKcZ9plUWvO7tw,8865
8
- anemoi/utils/dates.py,sha256=P-AW-dViogxn4t8Q10sO1HldJ2-lez6ILrx6z59tA10,7454
8
+ anemoi/utils/dates.py,sha256=yDIV8hYakNaC9iyYD9WRTNClFO6ZKyM8JKCRUiFJEv8,10413
9
9
  anemoi/utils/grib.py,sha256=gVfo4KYQv31iRyoqRDwk5tiqZDUgOIvhag_kO0qjYD0,3067
10
10
  anemoi/utils/hindcasts.py,sha256=X8k-81ltmkTDHdviY0SJgvMg7XDu07xoc5ALlUxyPoo,1453
11
- anemoi/utils/humanize.py,sha256=RzLLhN54UqtA5pUDj9CSU8wMuo9Sx_4CE--akCT7JZY,14973
11
+ anemoi/utils/humanize.py,sha256=-MRGgGG_STTWzIagL4RQnOAooqAIigCcuN-RZ6CK75M,14982
12
12
  anemoi/utils/provenance.py,sha256=NL36lM_aCw3fG6VIAouZCRBAJv8a6M3x9cScrFxCMcA,9579
13
- anemoi/utils/s3.py,sha256=VdugPD5QeyXTHjmYwpyGwmguWo0lwlNf4C3r1obVnFk,18500
14
- anemoi/utils/text.py,sha256=4Zlc4r9dzRjkKL9xqp2vuQsoJY15bJ3y_Xv3YW_XsmU,8510
15
- anemoi/utils/timer.py,sha256=JKOgFkpJxmVRn57DEBolmTGwr25P-ePTWASBd8CLeqM,972
13
+ anemoi/utils/s3.py,sha256=kDzbs4nVD2lQuppSe88NVSNpy0wSZpuzkmcAgN2irkU,18506
14
+ anemoi/utils/text.py,sha256=5HBqNwhifus4d3OUnod5q1VgCBdEpzE7o0IR0S85knw,10397
15
+ anemoi/utils/timer.py,sha256=EQcucuwUaGeSpt2S1APJlwSOu6kC47MK9f4h-r8c_AY,990
16
16
  anemoi/utils/commands/__init__.py,sha256=qAybFZPBBQs0dyx7dZ3X5JsLpE90pwrqt1vSV7cqEIw,706
17
17
  anemoi/utils/commands/config.py,sha256=KEffXZh0ZQfn8t6LXresfd94kDY0gEyulx9Wto5ttW0,824
18
18
  anemoi/utils/mars/__init__.py,sha256=RAeY8gJ7ZvsPlcIvrQ4fy9xVHs3SphTAPw_XJDtNIKo,1750
19
19
  anemoi/utils/mars/mars.yaml,sha256=R0dujp75lLA4wCWhPeOQnzJ45WZAYLT8gpx509cBFlc,66
20
- anemoi_utils-0.3.12.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
21
- anemoi_utils-0.3.12.dist-info/METADATA,sha256=oQ4jBfStHO26IYDrrlq6EpacxP9S-gCW3OyGOLpBs8Y,15514
22
- anemoi_utils-0.3.12.dist-info/WHEEL,sha256=Z4pYXqR_rTB7OWNDYFOm1qRk0RX6GFP2o8LgvP453Hk,91
23
- anemoi_utils-0.3.12.dist-info/entry_points.txt,sha256=LENOkn88xzFQo-V59AKoA_F_cfYQTJYtrNTtf37YgHY,60
24
- anemoi_utils-0.3.12.dist-info/top_level.txt,sha256=DYn8VPs-fNwr7fNH9XIBqeXIwiYYd2E2k5-dUFFqUz0,7
25
- anemoi_utils-0.3.12.dist-info/RECORD,,
20
+ anemoi_utils-0.3.14.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
21
+ anemoi_utils-0.3.14.dist-info/METADATA,sha256=CRVUi4_pMo2st6kwOUgQ0jyEFwl67ZcIHpDo7a4y2NE,15470
22
+ anemoi_utils-0.3.14.dist-info/WHEEL,sha256=Mdi9PDNwEZptOjTlUcAth7XJDFtKrHYaQMPulZeBCiQ,91
23
+ anemoi_utils-0.3.14.dist-info/entry_points.txt,sha256=LENOkn88xzFQo-V59AKoA_F_cfYQTJYtrNTtf37YgHY,60
24
+ anemoi_utils-0.3.14.dist-info/top_level.txt,sha256=DYn8VPs-fNwr7fNH9XIBqeXIwiYYd2E2k5-dUFFqUz0,7
25
+ anemoi_utils-0.3.14.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.3.0)
2
+ Generator: setuptools (73.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5