anemoi-utils 0.3.13__py3-none-any.whl → 0.3.15__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.13'
16
- __version_tuple__ = version_tuple = (0, 3, 13)
15
+ __version__ = version = '0.3.15'
16
+ __version_tuple__ = version_tuple = (0, 3, 15)
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,21 +8,14 @@
8
8
 
9
9
  import calendar
10
10
  import datetime
11
+ import re
11
12
 
12
- from .hindcasts import HindcastDatesTimes
13
-
14
-
15
- def normalise_frequency(frequency):
16
- if isinstance(frequency, int):
17
- return frequency
18
- assert isinstance(frequency, str), (type(frequency), frequency)
13
+ import isodate
19
14
 
20
- unit = frequency[-1].lower()
21
- v = int(frequency[:-1])
22
- return {"h": v, "d": v * 24}[unit]
15
+ from .hindcasts import HindcastDatesTimes
23
16
 
24
17
 
25
- def no_time_zone(date):
18
+ def _no_time_zone(date):
26
19
  """Remove time zone information from a date.
27
20
 
28
21
  Parameters
@@ -40,13 +33,15 @@ def no_time_zone(date):
40
33
 
41
34
 
42
35
  # this function is use in anemoi-datasets
43
- def as_datetime(date):
36
+ def as_datetime(date, keep_time_zone=False):
44
37
  """Convert a date to a datetime object, removing any time zone information.
45
38
 
46
39
  Parameters
47
40
  ----------
48
41
  date : datetime.date or datetime.datetime or str
49
42
  The date to convert.
43
+ keep_time_zone : bool, optional
44
+ If True, the time zone information is kept, by default False.
50
45
 
51
46
  Returns
52
47
  -------
@@ -54,18 +49,150 @@ def as_datetime(date):
54
49
  The datetime object.
55
50
  """
56
51
 
52
+ tidy = _no_time_zone if not keep_time_zone else lambda x: x
53
+
57
54
  if isinstance(date, datetime.datetime):
58
- return no_time_zone(date)
55
+ return tidy(date)
59
56
 
60
57
  if isinstance(date, datetime.date):
61
- return no_time_zone(datetime.datetime(date.year, date.month, date.day))
58
+ return tidy(datetime.datetime(date.year, date.month, date.day))
62
59
 
63
60
  if isinstance(date, str):
64
- return no_time_zone(datetime.datetime.fromisoformat(date))
61
+ return tidy(datetime.datetime.fromisoformat(date))
65
62
 
66
63
  raise ValueError(f"Invalid date type: {type(date)}")
67
64
 
68
65
 
66
+ def frequency_to_timedelta(frequency):
67
+ """Convert a frequency to a timedelta object.
68
+
69
+ Parameters
70
+ ----------
71
+ frequency : int or str or datetime.timedelta
72
+ The frequency to convert. If an integer, it is assumed to be in hours. If a string, it can be in the format:
73
+
74
+ - "1h" for 1 hour
75
+ - "1d" for 1 day
76
+ - "1m" for 1 minute
77
+ - "1s" for 1 second
78
+ - "1:30" for 1 hour and 30 minutes
79
+ - "1:30:10" for 1 hour, 30 minutes and 10 seconds
80
+ - "PT10M" for 10 minutes (ISO8601)
81
+
82
+ If a timedelta object is provided, it is returned as is.
83
+
84
+ Returns
85
+ -------
86
+ datetime.timedelta
87
+ The timedelta object.
88
+
89
+ Raises
90
+ ------
91
+ ValueError
92
+ Exception raised if the frequency cannot be converted to a timedelta.
93
+ """
94
+
95
+ if isinstance(frequency, datetime.timedelta):
96
+ return frequency
97
+
98
+ if isinstance(frequency, int):
99
+ return datetime.timedelta(hours=frequency)
100
+
101
+ assert isinstance(frequency, str), (type(frequency), frequency)
102
+
103
+ try:
104
+ return frequency_to_timedelta(int(frequency))
105
+ except ValueError:
106
+ pass
107
+
108
+ if re.match(r"^\d+[hdms]$", frequency, re.IGNORECASE):
109
+ unit = frequency[-1].lower()
110
+ v = int(frequency[:-1])
111
+ unit = {"h": "hours", "d": "days", "s": "seconds", "m": "minutes"}[unit]
112
+ return datetime.timedelta(**{unit: v})
113
+
114
+ m = frequency.split(":")
115
+ if len(m) == 2:
116
+ return datetime.timedelta(hours=int(m[0]), minutes=int(m[1]))
117
+
118
+ if len(m) == 3:
119
+ return datetime.timedelta(hours=int(m[0]), minutes=int(m[1]), seconds=int(m[2]))
120
+
121
+ # ISO8601
122
+ try:
123
+ return isodate.parse_duration(frequency)
124
+ except isodate.isoerror.ISO8601Error:
125
+ pass
126
+
127
+ raise ValueError(f"Cannot convert frequency {frequency} to timedelta")
128
+
129
+
130
+ def frequency_to_string(frequency):
131
+ """Convert a frequency (i.e. a datetime.timedelta) to a string.
132
+
133
+ Parameters
134
+ ----------
135
+ frequency : datetime.timedelta
136
+ The frequency to convert.
137
+
138
+ Returns
139
+ -------
140
+ str
141
+ A string representation of the frequency.
142
+ """
143
+
144
+ frequency = frequency_to_timedelta(frequency)
145
+
146
+ total_seconds = frequency.total_seconds()
147
+ assert int(total_seconds) == total_seconds, total_seconds
148
+ total_seconds = int(total_seconds)
149
+
150
+ seconds = total_seconds
151
+
152
+ days = seconds // (24 * 3600)
153
+ seconds %= 24 * 3600
154
+ hours = seconds // 3600
155
+ seconds %= 3600
156
+ minutes = seconds // 60
157
+ seconds %= 60
158
+
159
+ if days > 0 and hours == 0 and minutes == 0 and seconds == 0:
160
+ return f"{days}d"
161
+
162
+ if days == 0 and hours > 0 and minutes == 0 and seconds == 0:
163
+ return f"{hours}h"
164
+
165
+ if days == 0 and hours == 0 and minutes > 0 and seconds == 0:
166
+ return f"{minutes}m"
167
+
168
+ if days == 0 and hours == 0 and minutes == 0 and seconds > 0:
169
+ return f"{seconds}s"
170
+
171
+ if days > 0:
172
+ return f"{total_seconds}s"
173
+
174
+ return str(frequency)
175
+
176
+
177
+ def frequency_to_seconds(frequency):
178
+ """Convert a frequency to seconds.
179
+
180
+ Parameters
181
+ ----------
182
+ frequency : _type_
183
+ _description_
184
+
185
+ Returns
186
+ -------
187
+ int
188
+ Number of seconds.
189
+ """
190
+
191
+ result = frequency_to_timedelta(frequency).total_seconds()
192
+ assert int(result) == result, result
193
+ return int(result)
194
+
195
+
69
196
  DOW = {
70
197
  "monday": 0,
71
198
  "tuesday": 1,
@@ -142,7 +269,7 @@ class DateTimes:
142
269
  """
143
270
  self.start = as_datetime(start)
144
271
  self.end = as_datetime(end)
145
- self.increment = datetime.timedelta(hours=increment)
272
+ self.increment = frequency_to_timedelta(increment)
146
273
  self.day_of_month = _make_day(day_of_month)
147
274
  self.day_of_week = _make_week(day_of_week)
148
275
  self.calendar_months = _make_months(calendar_months)
@@ -273,8 +400,7 @@ def datetimes_factory(*args, **kwargs):
273
400
 
274
401
  kwargs = kwargs.copy()
275
402
  if "frequency" in kwargs:
276
- freq = kwargs.pop("frequency")
277
- kwargs["increment"] = normalise_frequency(freq)
403
+ kwargs["increment"] = kwargs.pop("frequency")
278
404
  return DateTimes(**kwargs)
279
405
 
280
406
  if not any((isinstance(x, dict) or isinstance(x, list)) for x in args):
anemoi/utils/humanize.py CHANGED
@@ -15,6 +15,8 @@ import re
15
15
  import warnings
16
16
  from collections import defaultdict
17
17
 
18
+ from anemoi.utils.dates import as_datetime
19
+
18
20
 
19
21
  def bytes_to_human(n: float) -> str:
20
22
  """Convert a number of bytes to a human readable string
@@ -625,3 +627,60 @@ def shorten_list(lst, max_length=5):
625
627
  if isinstance(lst, tuple):
626
628
  return tuple(result)
627
629
  return result
630
+
631
+
632
+ def _compress_dates(dates):
633
+ dates = sorted(dates)
634
+ if len(dates) < 3:
635
+ yield dates
636
+ return
637
+
638
+ prev = first = dates.pop(0)
639
+ curr = dates.pop(0)
640
+ delta = curr - prev
641
+ while curr - prev == delta:
642
+ prev = curr
643
+ if not dates:
644
+ break
645
+ curr = dates.pop(0)
646
+
647
+ yield (first, prev, delta)
648
+ if dates:
649
+ yield from _compress_dates([curr] + dates)
650
+
651
+
652
+ def compress_dates(dates):
653
+ """Compress a list of dates into a human-readable format.
654
+
655
+ Parameters
656
+ ----------
657
+ dates : list
658
+ A list of dates, as datetime objects or strings.
659
+
660
+ Returns
661
+ -------
662
+ str
663
+ A human-readable string representing the compressed dates.
664
+ """
665
+
666
+ dates = [as_datetime(_) for _ in dates]
667
+ result = []
668
+
669
+ for n in _compress_dates(dates):
670
+ if isinstance(n, list):
671
+ result.extend([str(_) for _ in n])
672
+ else:
673
+ result.append(" ".join([str(n[0]), "to", str(n[1]), "by", str(n[2])]))
674
+
675
+ return result
676
+
677
+
678
+ def print_dates(dates):
679
+ """Print a list of dates in a human-readable format.
680
+
681
+ Parameters
682
+ ----------
683
+ dates : list
684
+ A list of dates, as datetime objects or strings.
685
+ """
686
+ print(compress_dates(dates))
anemoi/utils/s3.py CHANGED
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: anemoi-utils
3
- Version: 0.3.13
3
+ Version: 0.3.15
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
@@ -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=drg-3xPikZkJrT_ckut_6xmzJvTagr6bFEW0FoqfxwQ,413
3
+ anemoi/utils/_version.py,sha256=9bARtG-NIsnxdzdFmq2OMzx2-WMRy4fvkJadsRpAZKk,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=6jLdWkPGmdCKyMJonB5MNPe_RuCBURm1Q0frjHqLk54,10655
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=-MRGgGG_STTWzIagL4RQnOAooqAIigCcuN-RZ6CK75M,14982
11
+ anemoi/utils/humanize.py,sha256=w1D17HqTT89xvNgeZlLDw_GNkWngEpv23JHk5A9tGtU,16265
12
12
  anemoi/utils/provenance.py,sha256=NL36lM_aCw3fG6VIAouZCRBAJv8a6M3x9cScrFxCMcA,9579
13
- anemoi/utils/s3.py,sha256=thSweHcsbp86CzGh_EMG98iGzJ4rRf2FeuQkXYv1xN8,18505
14
- anemoi/utils/text.py,sha256=4Zlc4r9dzRjkKL9xqp2vuQsoJY15bJ3y_Xv3YW_XsmU,8510
13
+ anemoi/utils/s3.py,sha256=kDzbs4nVD2lQuppSe88NVSNpy0wSZpuzkmcAgN2irkU,18506
14
+ anemoi/utils/text.py,sha256=5HBqNwhifus4d3OUnod5q1VgCBdEpzE7o0IR0S85knw,10397
15
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.13.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
21
- anemoi_utils-0.3.13.dist-info/METADATA,sha256=SBTqiIl_wfFDAOH_6Bu5Tol4LJV6ZE8AtuATA0n2Pyg,15447
22
- anemoi_utils-0.3.13.dist-info/WHEEL,sha256=-oYQCr74JF3a37z2nRlQays_SX2MqOANoqVjBBAP2yE,91
23
- anemoi_utils-0.3.13.dist-info/entry_points.txt,sha256=LENOkn88xzFQo-V59AKoA_F_cfYQTJYtrNTtf37YgHY,60
24
- anemoi_utils-0.3.13.dist-info/top_level.txt,sha256=DYn8VPs-fNwr7fNH9XIBqeXIwiYYd2E2k5-dUFFqUz0,7
25
- anemoi_utils-0.3.13.dist-info/RECORD,,
20
+ anemoi_utils-0.3.15.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
21
+ anemoi_utils-0.3.15.dist-info/METADATA,sha256=embyjlpZISW0daGqXYuZAJ9t4zHRe1IxfiDA213Mcis,15470
22
+ anemoi_utils-0.3.15.dist-info/WHEEL,sha256=Mdi9PDNwEZptOjTlUcAth7XJDFtKrHYaQMPulZeBCiQ,91
23
+ anemoi_utils-0.3.15.dist-info/entry_points.txt,sha256=LENOkn88xzFQo-V59AKoA_F_cfYQTJYtrNTtf37YgHY,60
24
+ anemoi_utils-0.3.15.dist-info/top_level.txt,sha256=DYn8VPs-fNwr7fNH9XIBqeXIwiYYd2E2k5-dUFFqUz0,7
25
+ anemoi_utils-0.3.15.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (71.0.3)
2
+ Generator: setuptools (73.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5