anemoi-utils 0.3.7__tar.gz → 0.3.9__tar.gz

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.

Files changed (52) hide show
  1. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/.github/workflows/python-publish.yml +1 -1
  2. {anemoi_utils-0.3.7/src/anemoi_utils.egg-info → anemoi_utils-0.3.9}/PKG-INFO +1 -1
  3. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi/utils/_version.py +2 -2
  4. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi/utils/cli.py +11 -2
  5. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi/utils/commands/config.py +2 -2
  6. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi/utils/config.py +120 -27
  7. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi/utils/dates.py +2 -31
  8. anemoi_utils-0.3.9/src/anemoi/utils/hindcasts.py +40 -0
  9. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi/utils/humanize.py +60 -2
  10. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi/utils/s3.py +12 -7
  11. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9/src/anemoi_utils.egg-info}/PKG-INFO +1 -1
  12. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi_utils.egg-info/SOURCES.txt +1 -0
  13. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/tests/test_utils.py +17 -1
  14. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/.gitignore +0 -0
  15. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/.pre-commit-config.yaml +0 -0
  16. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/.readthedocs.yaml +0 -0
  17. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/LICENSE +0 -0
  18. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/README.md +0 -0
  19. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/docs/Makefile +0 -0
  20. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/docs/_static/logo.png +0 -0
  21. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/docs/_static/style.css +0 -0
  22. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/docs/_templates/.gitkeep +0 -0
  23. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/docs/conf.py +0 -0
  24. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/docs/index.rst +0 -0
  25. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/docs/installing.rst +0 -0
  26. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/docs/modules/checkpoints.rst +0 -0
  27. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/docs/modules/config.rst +0 -0
  28. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/docs/modules/dates.rst +0 -0
  29. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/docs/modules/grib.rst +0 -0
  30. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/docs/modules/humanize.rst +0 -0
  31. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/docs/modules/provenance.rst +0 -0
  32. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/docs/modules/s3.rst +0 -0
  33. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/docs/modules/text.rst +0 -0
  34. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/docs/requirements.txt +0 -0
  35. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/pyproject.toml +0 -0
  36. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/setup.cfg +0 -0
  37. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi/utils/__init__.py +0 -0
  38. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi/utils/__main__.py +0 -0
  39. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi/utils/caching.py +0 -0
  40. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi/utils/checkpoints.py +0 -0
  41. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi/utils/commands/__init__.py +0 -0
  42. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi/utils/grib.py +0 -0
  43. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi/utils/mars/__init__.py +0 -0
  44. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi/utils/mars/mars.yaml +0 -0
  45. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi/utils/provenance.py +0 -0
  46. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi/utils/text.py +0 -0
  47. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi/utils/timer.py +0 -0
  48. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi_utils.egg-info/dependency_links.txt +0 -0
  49. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi_utils.egg-info/entry_points.txt +0 -0
  50. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi_utils.egg-info/requires.txt +0 -0
  51. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/src/anemoi_utils.egg-info/top_level.txt +0 -0
  52. {anemoi_utils-0.3.7 → anemoi_utils-0.3.9}/tests/test_dates.py +0 -0
@@ -6,7 +6,7 @@ name: Upload Python Package
6
6
  on:
7
7
 
8
8
  push: {}
9
-
9
+ pull_request:
10
10
  release:
11
11
  types: [created]
12
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: anemoi-utils
3
- Version: 0.3.7
3
+ Version: 0.3.9
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
@@ -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.7'
16
- __version_tuple__ = version_tuple = (0, 3, 7)
15
+ __version__ = version = '0.3.9'
16
+ __version_tuple__ = version_tuple = (0, 3, 9)
@@ -16,6 +16,8 @@ LOG = logging.getLogger(__name__)
16
16
 
17
17
 
18
18
  class Command:
19
+ accept_unknown_args = False
20
+
19
21
  def run(self, args):
20
22
  raise NotImplementedError(f"Command not implemented: {args.command}")
21
23
 
@@ -97,7 +99,7 @@ def register_commands(here, package, select, fail=None):
97
99
 
98
100
  def cli_main(version, description, commands):
99
101
  parser = make_parser(description, commands)
100
- args = parser.parse_args()
102
+ args, unknown = parser.parse_known_args()
101
103
 
102
104
  if args.version:
103
105
  print(version)
@@ -115,8 +117,15 @@ def cli_main(version, description, commands):
115
117
  level=logging.DEBUG if args.debug else logging.INFO,
116
118
  )
117
119
 
120
+ if unknown and not cmd.accept_unknown_args:
121
+ # This should trigger an error
122
+ parser.parse_args()
123
+
118
124
  try:
119
- cmd.run(args)
125
+ if unknown:
126
+ cmd.run(args, unknown)
127
+ else:
128
+ cmd.run(args)
120
129
  except ValueError as e:
121
130
  traceback.print_exc()
122
131
  LOG.error("\n💣 %s", str(e).lstrip())
@@ -11,7 +11,7 @@
11
11
 
12
12
  import json
13
13
 
14
- from ..config import config_path
14
+ from ..config import _config_path
15
15
  from ..config import load_config
16
16
  from . import Command
17
17
 
@@ -23,7 +23,7 @@ class Config(Command):
23
23
 
24
24
  def run(self, args):
25
25
  if args.path:
26
- print(config_path())
26
+ print(_config_path())
27
27
  else:
28
28
  print(json.dumps(load_config(), indent=4))
29
29
 
@@ -104,12 +104,54 @@ class DotDict(dict):
104
104
 
105
105
  CONFIG = {}
106
106
  CHECKED = {}
107
- CONFIG_LOCK = threading.Lock()
107
+ CONFIG_LOCK = threading.RLock()
108
108
  QUIET = False
109
109
 
110
110
 
111
- def config_path(name="settings.toml"):
111
+ def _find(config, what, result=None):
112
+ if result is None:
113
+ result = []
114
+
115
+ if isinstance(config, list):
116
+ for i in config:
117
+ _find(i, what, result)
118
+ return result
119
+
120
+ if isinstance(config, dict):
121
+ if what in config:
122
+ result.append(config[what])
123
+
124
+ for k, v in config.items():
125
+ _find(v, what, result)
126
+
127
+ return result
128
+
129
+
130
+ def _merge_dicts(a, b):
131
+ for k, v in b.items():
132
+ if k in a and isinstance(a[k], dict) and isinstance(v, dict):
133
+ _merge_dicts(a[k], v)
134
+ else:
135
+ a[k] = v
136
+
137
+
138
+ def _set_defaults(a, b):
139
+ for k, v in b.items():
140
+ if k in a and isinstance(a[k], dict) and isinstance(v, dict):
141
+ _set_defaults(a[k], v)
142
+ else:
143
+ a.setdefault(k, v)
144
+
145
+
146
+ def _config_path(name="settings.toml"):
112
147
  global QUIET
148
+
149
+ if name.startswith("/") or name.startswith("."):
150
+ return name
151
+
152
+ if name.startswith("~"):
153
+ return os.path.expanduser(name)
154
+
113
155
  full = os.path.join(os.path.expanduser("~"), ".config", "anemoi", name)
114
156
  os.makedirs(os.path.dirname(full), exist_ok=True)
115
157
 
@@ -133,46 +175,77 @@ def config_path(name="settings.toml"):
133
175
  return full
134
176
 
135
177
 
136
- def _load(path):
137
- if path.endswith(".json"):
138
- with open(path, "rb") as f:
139
- return json.load(f)
178
+ def load_any_dict_format(path):
179
+ """Load a configuration file in any supported format: JSON, YAML and TOML.
180
+
181
+ Returns
182
+ -------
183
+ dict
184
+ The decoded configuration file.
185
+ """
186
+
187
+ try:
188
+ if path.endswith(".json"):
189
+ with open(path, "rb") as f:
190
+ return json.load(f)
140
191
 
141
- if path.endswith(".yaml") or path.endswith(".yml"):
142
- with open(path, "rb") as f:
143
- return yaml.safe_load(f)
192
+ if path.endswith(".yaml") or path.endswith(".yml"):
193
+ with open(path, "rb") as f:
194
+ return yaml.safe_load(f)
144
195
 
145
- if path.endswith(".toml"):
146
- with open(path, "rb") as f:
147
- return tomllib.load(f)
196
+ if path.endswith(".toml"):
197
+ with open(path, "rb") as f:
198
+ return tomllib.load(f)
199
+ except (json.JSONDecodeError, yaml.YAMLError, tomllib.TOMLDecodeError) as e:
200
+ LOG.warning(f"Failed to parse config file {path}", exc_info=e)
201
+ return ValueError(f"Failed to parse config file {path} [{e}]")
148
202
 
149
203
  return open(path).read()
150
204
 
151
205
 
152
- def _load_config(name="settings.toml"):
206
+ def _load_config(name="settings.toml", secrets=None, defaults=None):
153
207
 
154
208
  if name in CONFIG:
155
209
  return CONFIG[name]
156
210
 
157
- conf = config_path(name)
158
-
159
- if os.path.exists(conf):
160
- config = _load(conf)
211
+ path = _config_path(name)
212
+ if os.path.exists(path):
213
+ config = load_any_dict_format(path)
161
214
  else:
162
215
  config = {}
163
216
 
164
- if isinstance(config, dict):
165
- CONFIG[name] = DotDict(config)
166
- else:
167
- CONFIG[name] = config
217
+ if defaults is not None:
218
+ if isinstance(defaults, str):
219
+ defaults = load_raw_config(defaults)
220
+ _set_defaults(config, defaults)
221
+
222
+ if secrets is not None:
223
+ if isinstance(secrets, str):
224
+ secrets = [secrets]
225
+
226
+ base, ext = os.path.splitext(path)
227
+ secret_name = base + ".secrets" + ext
228
+
229
+ found = set()
230
+ for secret in secrets:
231
+ if _find(config, secret):
232
+ found.add(secret)
233
+
234
+ if found:
235
+ check_config_mode(name, secret_name, found)
236
+
237
+ check_config_mode(secret_name, None)
238
+ secret_config = _load_config(secret_name)
239
+ _merge_dicts(config, secret_config)
168
240
 
241
+ CONFIG[name] = DotDict(config)
169
242
  return CONFIG[name]
170
243
 
171
244
 
172
245
  def _save_config(name, data):
173
246
  CONFIG.pop(name, None)
174
247
 
175
- conf = config_path(name)
248
+ conf = _config_path(name)
176
249
 
177
250
  if conf.endswith(".json"):
178
251
  with open(conf, "w") as f:
@@ -207,7 +280,7 @@ def save_config(name, data):
207
280
  _save_config(name, data)
208
281
 
209
282
 
210
- def load_config(name="settings.toml"):
283
+ def load_config(name="settings.toml", secrets=None, defaults=None):
211
284
  """Read a configuration file.
212
285
 
213
286
  Parameters
@@ -220,11 +293,21 @@ def load_config(name="settings.toml"):
220
293
  DotDict or str
221
294
  Return DotDict if it is a dictionary, otherwise the raw data
222
295
  """
296
+
223
297
  with CONFIG_LOCK:
224
- return _load_config(name)
298
+ return _load_config(name, secrets, defaults)
299
+
300
+
301
+ def load_raw_config(name, default=None):
302
+
303
+ path = _config_path(name)
304
+ if os.path.exists(path):
305
+ return load_any_dict_format(path)
306
+
307
+ return default
225
308
 
226
309
 
227
- def check_config_mode(name="settings.toml"):
310
+ def check_config_mode(name="settings.toml", secrets_name=None, secrets=None):
228
311
  """Check that a configuration file is secure.
229
312
 
230
313
  Parameters
@@ -240,8 +323,18 @@ def check_config_mode(name="settings.toml"):
240
323
  with CONFIG_LOCK:
241
324
  if name in CHECKED:
242
325
  return
243
- conf = config_path(name)
326
+
327
+ conf = _config_path(name)
328
+ if not os.path.exists(conf):
329
+ return
244
330
  mode = os.stat(conf).st_mode
245
331
  if mode & 0o777 != 0o600:
246
- raise SystemError(f"Configuration file {conf} is not secure. " "Please run `chmod 600 {conf}`.")
332
+ if secrets_name:
333
+ secret_path = _config_path(secrets_name)
334
+ raise SystemError(
335
+ f"Configuration file {conf} should not hold entries {secrets}.\n"
336
+ f"Please move them to {secret_path}."
337
+ )
338
+ raise SystemError(f"Configuration file {conf} is not secure.\n" f"Please run `chmod 600 {conf}`.")
339
+
247
340
  CHECKED[name] = True
@@ -9,6 +9,8 @@
9
9
  import calendar
10
10
  import datetime
11
11
 
12
+ from .hindcasts import HindcastDatesTimes
13
+
12
14
 
13
15
  def normalise_frequency(frequency):
14
16
  if isinstance(frequency, int):
@@ -158,37 +160,6 @@ class DateTimes:
158
160
  date += self.increment
159
161
 
160
162
 
161
- class HindcastDatesTimes:
162
- """The HindcastDatesTimes class is an iterator that generates datetime objects within a given range."""
163
-
164
- def __init__(self, reference_dates, years=20):
165
- """_summary_
166
-
167
- Parameters
168
- ----------
169
- reference_dates : _type_
170
- _description_
171
- years : int, optional
172
- _description_, by default 20
173
- """
174
-
175
- self.reference_dates = reference_dates
176
-
177
- if isinstance(years, list):
178
- self.years = years
179
- else:
180
- self.years = range(1, years + 1)
181
-
182
- def __iter__(self):
183
- for reference_date in self.reference_dates:
184
- for year in self.years:
185
- if reference_date.month == 2 and reference_date.day == 29:
186
- date = datetime.datetime(reference_date.year - year, 2, 28)
187
- else:
188
- date = datetime.datetime(reference_date.year - year, reference_date.month, reference_date.day)
189
- yield (date, reference_date)
190
-
191
-
192
163
  class Year(DateTimes):
193
164
  """Year is defined as the months of January to December."""
194
165
 
@@ -0,0 +1,40 @@
1
+ # (C) Copyright 2024 European Centre for Medium-Range Weather Forecasts.
2
+ # This software is licensed under the terms of the Apache Licence Version 2.0
3
+ # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
4
+ # In applying this licence, ECMWF does not waive the privileges and immunities
5
+ # granted to it by virtue of its status as an intergovernmental organisation
6
+ # nor does it submit to any jurisdiction.
7
+
8
+
9
+ import datetime
10
+
11
+
12
+ class HindcastDatesTimes:
13
+ """The HindcastDatesTimes class is an iterator that generates datetime objects within a given range."""
14
+
15
+ def __init__(self, reference_dates, years=20):
16
+ """_summary_
17
+
18
+ Parameters
19
+ ----------
20
+ reference_dates : _type_
21
+ _description_
22
+ years : int, optional
23
+ _description_, by default 20
24
+ """
25
+
26
+ self.reference_dates = reference_dates
27
+
28
+ if isinstance(years, list):
29
+ self.years = years
30
+ else:
31
+ self.years = range(1, years + 1)
32
+
33
+ def __iter__(self):
34
+ for reference_date in self.reference_dates:
35
+ for year in self.years:
36
+ if reference_date.month == 2 and reference_date.day == 29:
37
+ date = datetime.datetime(reference_date.year - year, 2, 28)
38
+ else:
39
+ date = datetime.datetime(reference_date.year - year, reference_date.month, reference_date.day)
40
+ yield (date, reference_date)
@@ -10,6 +10,7 @@
10
10
  """Generate human readable strings"""
11
11
 
12
12
  import datetime
13
+ import json
13
14
  import re
14
15
  from collections import defaultdict
15
16
 
@@ -189,7 +190,7 @@ def __(n):
189
190
  return "th"
190
191
 
191
192
 
192
- def when(then, now=None, short=True):
193
+ def when(then, now=None, short=True, use_utc=False):
193
194
  """Generate a human readable string for a date, relative to now
194
195
 
195
196
  >>> when(datetime.datetime.now() - datetime.timedelta(hours=2))
@@ -225,7 +226,10 @@ def when(then, now=None, short=True):
225
226
  last = "last"
226
227
 
227
228
  if now is None:
228
- now = datetime.datetime.now()
229
+ if use_utc:
230
+ now = datetime.datetime.utcnow()
231
+ else:
232
+ now = datetime.datetime.now()
229
233
 
230
234
  diff = (now - then).total_seconds()
231
235
 
@@ -472,3 +476,57 @@ def rounded_datetime(d):
472
476
  d = d + datetime.timedelta(seconds=1)
473
477
  d = d.replace(microsecond=0)
474
478
  return d
479
+
480
+
481
+ def json_pretty_dump(obj, max_line_length=120, default=str):
482
+ """Custom JSON dump function that keeps dicts and lists on one line if they are short enough.
483
+
484
+ Parameters
485
+ ----------
486
+ obj
487
+ The object to be dumped as JSON.
488
+ max_line_length
489
+ Maximum allowed line length for pretty-printing.
490
+
491
+ Returns
492
+ -------
493
+ unknown
494
+ JSON string.
495
+ """
496
+
497
+ def _format_json(obj, indent_level=0):
498
+ """Helper function to format JSON objects with custom pretty-print rules.
499
+
500
+ Parameters
501
+ ----------
502
+ obj
503
+ The object to format.
504
+ indent_level
505
+ Current indentation level.
506
+
507
+ Returns
508
+ -------
509
+ unknown
510
+ Formatted JSON string.
511
+ """
512
+ indent = " " * 4 * indent_level
513
+ if isinstance(obj, dict):
514
+ items = []
515
+ for key, value in obj.items():
516
+ items.append(f'"{key}": {_format_json(value, indent_level + 1)}')
517
+ line = "{" + ", ".join(items) + "}"
518
+ if len(line) <= max_line_length:
519
+ return line
520
+ else:
521
+ return "{\n" + ",\n".join([f"{indent} {item}" for item in items]) + "\n" + indent + "}"
522
+ elif isinstance(obj, list):
523
+ items = [_format_json(item, indent_level + 1) for item in obj]
524
+ line = "[" + ", ".join(items) + "]"
525
+ if len(line) <= max_line_length:
526
+ return line
527
+ else:
528
+ return "[\n" + ",\n".join([f"{indent} {item}" for item in items]) + "\n" + indent + "]"
529
+ else:
530
+ return json.dumps(obj, default=default)
531
+
532
+ return _format_json(obj)
@@ -18,7 +18,7 @@ to use a different S3 compatible service::
18
18
 
19
19
  """
20
20
 
21
- import concurrent
21
+ import concurrent.futures
22
22
  import logging
23
23
  import os
24
24
  import threading
@@ -26,7 +26,6 @@ from copy import deepcopy
26
26
 
27
27
  import tqdm
28
28
 
29
- from .config import check_config_mode
30
29
  from .config import load_config
31
30
  from .humanize import bytes
32
31
 
@@ -41,9 +40,7 @@ thread_local = threading.local()
41
40
  def s3_client(bucket):
42
41
  import boto3
43
42
 
44
- config = load_config()
45
- if "object-storage" in config:
46
- check_config_mode()
43
+ config = load_config(secrets=["aws_access_key_id", "aws_secret_access_key"])
47
44
 
48
45
  if not hasattr(thread_local, "s3_clients"):
49
46
  thread_local.s3_clients = {}
@@ -51,8 +48,14 @@ def s3_client(bucket):
51
48
  if bucket not in thread_local.s3_clients:
52
49
 
53
50
  options = {}
54
- options.update(config.get("object-storage", {}))
55
- options.update(config.get("object-storage", {}).get(bucket, {}))
51
+ cfg = config.get("object-storage", {})
52
+ for k, v in cfg.items():
53
+ if isinstance(v, (str, int, float, bool)):
54
+ options[k] = v
55
+
56
+ for k, v in cfg.get(bucket, {}).items():
57
+ if isinstance(v, (str, int, float, bool)):
58
+ options[k] = v
56
59
 
57
60
  type = options.pop("type", "s3")
58
61
  if type != "s3":
@@ -420,11 +423,13 @@ def list_folder(folder):
420
423
  A list of the subfolders names in the folder.
421
424
  """
422
425
 
426
+ print(folder)
423
427
  assert folder.startswith("s3://")
424
428
  if not folder.endswith("/"):
425
429
  folder += "/"
426
430
 
427
431
  _, _, bucket, prefix = folder.split("/", 3)
432
+ print(bucket, prefix)
428
433
 
429
434
  s3 = s3_client(bucket)
430
435
  paginator = s3.get_paginator("list_objects_v2")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: anemoi-utils
3
- Version: 0.3.7
3
+ Version: 0.3.9
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
@@ -30,6 +30,7 @@ src/anemoi/utils/cli.py
30
30
  src/anemoi/utils/config.py
31
31
  src/anemoi/utils/dates.py
32
32
  src/anemoi/utils/grib.py
33
+ src/anemoi/utils/hindcasts.py
33
34
  src/anemoi/utils/humanize.py
34
35
  src/anemoi/utils/provenance.py
35
36
  src/anemoi/utils/s3.py
@@ -7,6 +7,8 @@
7
7
 
8
8
 
9
9
  from anemoi.utils.config import DotDict
10
+ from anemoi.utils.config import _merge_dicts
11
+ from anemoi.utils.config import _set_defaults
10
12
  from anemoi.utils.grib import paramid_to_shortname
11
13
  from anemoi.utils.grib import shortname_to_paramid
12
14
 
@@ -30,10 +32,24 @@ def test_dotdict():
30
32
  assert d.e[1].a == 3
31
33
 
32
34
 
35
+ def test_merge_dicts():
36
+ a = dict(a=1, b=2, c=dict(d=3, e=4))
37
+ b = dict(a=10, c=dict(a=30, e=40), d=9)
38
+ _merge_dicts(a, b)
39
+ assert a == {"a": 10, "b": 2, "c": {"d": 3, "e": 40, "a": 30}, "d": 9}
40
+
41
+
42
+ def test_set_defaults():
43
+ a = dict(a=1, b=2, c=dict(d=3, e=4))
44
+ b = dict(a=10, c=dict(a=30, e=40), d=9)
45
+ _set_defaults(a, b)
46
+ assert a == {"a": 1, "b": 2, "c": {"d": 3, "e": 4, "a": 30}, "d": 9}
47
+
48
+
33
49
  def test_grib():
34
50
  assert shortname_to_paramid("2t") == 167
35
51
  assert paramid_to_shortname(167) == "2t"
36
52
 
37
53
 
38
54
  if __name__ == "__main__":
39
- test_grib()
55
+ test_set_defaults()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes