anemoi-utils 0.3.6__tar.gz → 0.3.8__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 (54) hide show
  1. {anemoi_utils-0.3.6/src/anemoi_utils.egg-info → anemoi_utils-0.3.8}/PKG-INFO +1 -1
  2. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/pyproject.toml +1 -0
  3. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi/utils/_version.py +2 -2
  4. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi/utils/cli.py +9 -2
  5. anemoi_utils-0.3.8/src/anemoi/utils/commands/config.py +31 -0
  6. anemoi_utils-0.3.8/src/anemoi/utils/config.py +260 -0
  7. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi/utils/dates.py +2 -31
  8. anemoi_utils-0.3.8/src/anemoi/utils/hindcasts.py +40 -0
  9. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi/utils/humanize.py +55 -0
  10. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi/utils/s3.py +16 -0
  11. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8/src/anemoi_utils.egg-info}/PKG-INFO +1 -1
  12. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi_utils.egg-info/SOURCES.txt +2 -1
  13. anemoi_utils-0.3.6/src/anemoi/utils/commands/checkpoint.py +0 -61
  14. anemoi_utils-0.3.6/src/anemoi/utils/config.py +0 -142
  15. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/.github/workflows/python-publish.yml +0 -0
  16. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/.gitignore +0 -0
  17. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/.pre-commit-config.yaml +0 -0
  18. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/.readthedocs.yaml +0 -0
  19. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/LICENSE +0 -0
  20. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/README.md +0 -0
  21. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/docs/Makefile +0 -0
  22. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/docs/_static/logo.png +0 -0
  23. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/docs/_static/style.css +0 -0
  24. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/docs/_templates/.gitkeep +0 -0
  25. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/docs/conf.py +0 -0
  26. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/docs/index.rst +0 -0
  27. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/docs/installing.rst +0 -0
  28. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/docs/modules/checkpoints.rst +0 -0
  29. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/docs/modules/config.rst +0 -0
  30. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/docs/modules/dates.rst +0 -0
  31. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/docs/modules/grib.rst +0 -0
  32. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/docs/modules/humanize.rst +0 -0
  33. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/docs/modules/provenance.rst +0 -0
  34. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/docs/modules/s3.rst +0 -0
  35. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/docs/modules/text.rst +0 -0
  36. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/docs/requirements.txt +0 -0
  37. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/setup.cfg +0 -0
  38. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi/utils/__init__.py +0 -0
  39. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi/utils/__main__.py +0 -0
  40. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi/utils/caching.py +0 -0
  41. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi/utils/checkpoints.py +0 -0
  42. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi/utils/commands/__init__.py +0 -0
  43. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi/utils/grib.py +0 -0
  44. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi/utils/mars/__init__.py +0 -0
  45. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi/utils/mars/mars.yaml +0 -0
  46. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi/utils/provenance.py +0 -0
  47. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi/utils/text.py +0 -0
  48. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi/utils/timer.py +0 -0
  49. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi_utils.egg-info/dependency_links.txt +0 -0
  50. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi_utils.egg-info/entry_points.txt +0 -0
  51. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi_utils.egg-info/requires.txt +0 -0
  52. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/src/anemoi_utils.egg-info/top_level.txt +0 -0
  53. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/tests/test_dates.py +0 -0
  54. {anemoi_utils-0.3.6 → anemoi_utils-0.3.8}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: anemoi-utils
3
- Version: 0.3.6
3
+ Version: 0.3.8
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
@@ -104,6 +104,7 @@ urls.Documentation = "https://anemoi-utils.readthedocs.io/"
104
104
  urls.Homepage = "https://github.com/ecmwf/anemoi-utils/"
105
105
  urls.Issues = "https://github.com/ecmwf/anemoi-utils/issues"
106
106
  urls.Repository = "https://github.com/ecmwf/anemoi-utils/"
107
+
107
108
  scripts.anemoi-utils = "anemoi.utils.__main__:main"
108
109
 
109
110
  [tool.setuptools.package-data]
@@ -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.6'
16
- __version_tuple__ = version_tuple = (0, 3, 6)
15
+ __version__ = version = '0.3.8'
16
+ __version_tuple__ = version_tuple = (0, 3, 8)
@@ -97,7 +97,7 @@ def register_commands(here, package, select, fail=None):
97
97
 
98
98
  def cli_main(version, description, commands):
99
99
  parser = make_parser(description, commands)
100
- args = parser.parse_args()
100
+ args, unknown = parser.parse_known_args()
101
101
 
102
102
  if args.version:
103
103
  print(version)
@@ -115,8 +115,15 @@ def cli_main(version, description, commands):
115
115
  level=logging.DEBUG if args.debug else logging.INFO,
116
116
  )
117
117
 
118
+ if unknown and not cmd.accept_unknown_args:
119
+ # This should trigger an error
120
+ parser.parse_args()
121
+
118
122
  try:
119
- cmd.run(args)
123
+ if unknown:
124
+ cmd.run(args, unknown)
125
+ else:
126
+ cmd.run(args)
120
127
  except ValueError as e:
121
128
  traceback.print_exc()
122
129
  LOG.error("\n💣 %s", str(e).lstrip())
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env python
2
+ # (C) Copyright 2024 ECMWF.
3
+ #
4
+ # This software is licensed under the terms of the Apache Licence Version 2.0
5
+ # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
6
+ # In applying this licence, ECMWF does not waive the privileges and immunities
7
+ # granted to it by virtue of its status as an intergovernmental organisation
8
+ # nor does it submit to any jurisdiction.
9
+ #
10
+
11
+
12
+ import json
13
+
14
+ from ..config import config_path
15
+ from ..config import load_config
16
+ from . import Command
17
+
18
+
19
+ class Config(Command):
20
+
21
+ def add_arguments(self, command_parser):
22
+ command_parser.add_argument("--path", help="Print path to config file")
23
+
24
+ def run(self, args):
25
+ if args.path:
26
+ print(config_path())
27
+ else:
28
+ print(json.dumps(load_config(), indent=4))
29
+
30
+
31
+ command = Config
@@ -0,0 +1,260 @@
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 json
10
+ import logging
11
+ import os
12
+ import threading
13
+
14
+ import yaml
15
+
16
+ try:
17
+ import tomllib # Only available since 3.11
18
+ except ImportError:
19
+ import tomli as tomllib
20
+
21
+
22
+ LOG = logging.getLogger(__name__)
23
+
24
+
25
+ class DotDict(dict):
26
+ """A dictionary that allows access to its keys as attributes.
27
+
28
+ >>> d = DotDict({"a": 1, "b": {"c": 2}})
29
+ >>> d.a
30
+ 1
31
+ >>> d.b.c
32
+ 2
33
+ >>> d.b = 3
34
+ >>> d.b
35
+ 3
36
+
37
+ The class is recursive, so nested dictionaries are also DotDicts.
38
+
39
+ The DotDict class has the same constructor as the dict class.
40
+
41
+ >>> d = DotDict(a=1, b=2)
42
+
43
+ """
44
+
45
+ def __init__(self, *args, **kwargs):
46
+ super().__init__(*args, **kwargs)
47
+
48
+ for k, v in self.items():
49
+ if isinstance(v, dict):
50
+ self[k] = DotDict(v)
51
+
52
+ if isinstance(v, list):
53
+ self[k] = [DotDict(i) if isinstance(i, dict) else i for i in v]
54
+
55
+ if isinstance(v, tuple):
56
+ self[k] = [DotDict(i) if isinstance(i, dict) else i for i in v]
57
+
58
+ @classmethod
59
+ def from_file(cls, path: str):
60
+ _, ext = os.path.splitext(path)
61
+ if ext == ".yaml" or ext == ".yml":
62
+ return cls.from_yaml_file(path)
63
+ elif ext == ".json":
64
+ return cls.from_json_file(path)
65
+ elif ext == ".toml":
66
+ return cls.from_toml_file(path)
67
+ else:
68
+ raise ValueError(f"Unknown file extension {ext}")
69
+
70
+ @classmethod
71
+ def from_yaml_file(cls, path: str):
72
+ with open(path, "r") as file:
73
+ data = yaml.safe_load(file)
74
+
75
+ return cls(data)
76
+
77
+ @classmethod
78
+ def from_json_file(cls, path: str):
79
+ with open(path, "r") as file:
80
+ data = json.load(file)
81
+
82
+ return cls(data)
83
+
84
+ @classmethod
85
+ def from_toml_file(cls, path: str):
86
+ with open(path, "r") as file:
87
+ data = tomllib.load(file)
88
+ return cls(data)
89
+
90
+ def __getattr__(self, attr):
91
+ try:
92
+ return self[attr]
93
+ except KeyError:
94
+ raise AttributeError(attr)
95
+
96
+ def __setattr__(self, attr, value):
97
+ if isinstance(value, dict):
98
+ value = DotDict(value)
99
+ self[attr] = value
100
+
101
+ def __repr__(self) -> str:
102
+ return f"DotDict({super().__repr__()})"
103
+
104
+
105
+ CONFIG = {}
106
+ CHECKED = {}
107
+ CONFIG_LOCK = threading.Lock()
108
+ QUIET = False
109
+
110
+
111
+ def config_path(name="settings.toml"):
112
+ global QUIET
113
+ full = os.path.join(os.path.expanduser("~"), ".config", "anemoi", name)
114
+ os.makedirs(os.path.dirname(full), exist_ok=True)
115
+
116
+ if name == "settings.toml":
117
+ old = os.path.join(os.path.expanduser("~"), ".anemoi.toml")
118
+ if not os.path.exists(full) and os.path.exists(old):
119
+ if not QUIET:
120
+ LOG.warning(
121
+ "Configuration file found at ~/.anemoi.toml. Please move it to ~/.config/anemoi/settings.toml"
122
+ )
123
+ QUIET = True
124
+ return old
125
+ else:
126
+ if os.path.exists(old):
127
+ if not QUIET:
128
+ LOG.warning(
129
+ "Configuration file found at ~/.anemoi.toml and ~/.config/anemoi/settings.toml, ignoring the former"
130
+ )
131
+ QUIET = True
132
+
133
+ return full
134
+
135
+
136
+ def _load(path):
137
+ try:
138
+ if path.endswith(".json"):
139
+ with open(path, "rb") as f:
140
+ return json.load(f)
141
+
142
+ if path.endswith(".yaml") or path.endswith(".yml"):
143
+ with open(path, "rb") as f:
144
+ return yaml.safe_load(f)
145
+
146
+ if path.endswith(".toml"):
147
+ with open(path, "rb") as f:
148
+ return tomllib.load(f)
149
+ except (json.JSONDecodeError, yaml.YAMLError, tomllib.TOMLDecodeError) as e:
150
+ LOG.warning(f"Failed to parse config file {path}", exc_info=e)
151
+ return ValueError(f"Failed to parse config file {path} [{e}]")
152
+
153
+ return open(path).read()
154
+
155
+
156
+ def _load_config(name="settings.toml"):
157
+
158
+ if name in CONFIG:
159
+ return CONFIG[name]
160
+
161
+ conf = config_path(name)
162
+
163
+ if os.path.exists(conf):
164
+ config = _load(conf)
165
+ else:
166
+ config = {}
167
+
168
+ if isinstance(config, dict):
169
+ CONFIG[name] = DotDict(config)
170
+ else:
171
+ CONFIG[name] = config
172
+
173
+ return CONFIG[name]
174
+
175
+
176
+ def _save_config(name, data):
177
+ CONFIG.pop(name, None)
178
+
179
+ conf = config_path(name)
180
+
181
+ if conf.endswith(".json"):
182
+ with open(conf, "w") as f:
183
+ json.dump(data, f, indent=4)
184
+ return
185
+
186
+ if conf.endswith(".yaml") or conf.endswith(".yml"):
187
+ with open(conf, "w") as f:
188
+ yaml.dump(data, f)
189
+ return
190
+
191
+ if conf.endswith(".toml"):
192
+ raise NotImplementedError("Saving to TOML is not implemented yet")
193
+
194
+ with open(conf, "w") as f:
195
+ f.write(data)
196
+
197
+
198
+ def save_config(name, data):
199
+ """Save a configuration file.
200
+
201
+ Parameters
202
+ ----------
203
+ name : str
204
+ The name of the configuration file to save.
205
+
206
+ data : Any
207
+ The data to save.
208
+
209
+ """
210
+ with CONFIG_LOCK:
211
+ _save_config(name, data)
212
+
213
+
214
+ def load_config(name="settings.toml"):
215
+ """Read a configuration file.
216
+
217
+ Parameters
218
+ ----------
219
+ name : str, optional
220
+ The name of the config file to read, by default "settings.toml"
221
+
222
+ Returns
223
+ -------
224
+ DotDict or str
225
+ Return DotDict if it is a dictionary, otherwise the raw data
226
+ """
227
+ with CONFIG_LOCK:
228
+ return _load_config(name)
229
+
230
+
231
+ def load_raw_config(name, default=None):
232
+
233
+ path = config_path(name)
234
+ if os.path.exists(path):
235
+ return _load(path)
236
+
237
+ return default
238
+
239
+
240
+ def check_config_mode(name="settings.toml"):
241
+ """Check that a configuration file is secure.
242
+
243
+ Parameters
244
+ ----------
245
+ name : str, optional
246
+ The name of the configuration file, by default "settings.toml"
247
+
248
+ Raises
249
+ ------
250
+ SystemError
251
+ If the configuration file is not secure.
252
+ """
253
+ with CONFIG_LOCK:
254
+ if name in CHECKED:
255
+ return
256
+ conf = config_path(name)
257
+ mode = os.stat(conf).st_mode
258
+ if mode & 0o777 != 0o600:
259
+ raise SystemError(f"Configuration file {conf} is not secure. " "Please run `chmod 600 {conf}`.")
260
+ 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
 
@@ -472,3 +473,57 @@ def rounded_datetime(d):
472
473
  d = d + datetime.timedelta(seconds=1)
473
474
  d = d.replace(microsecond=0)
474
475
  return d
476
+
477
+
478
+ def json_pretty_dump(obj, max_line_length=120, default=str):
479
+ """Custom JSON dump function that keeps dicts and lists on one line if they are short enough.
480
+
481
+ Parameters
482
+ ----------
483
+ obj
484
+ The object to be dumped as JSON.
485
+ max_line_length
486
+ Maximum allowed line length for pretty-printing.
487
+
488
+ Returns
489
+ -------
490
+ unknown
491
+ JSON string.
492
+ """
493
+
494
+ def _format_json(obj, indent_level=0):
495
+ """Helper function to format JSON objects with custom pretty-print rules.
496
+
497
+ Parameters
498
+ ----------
499
+ obj
500
+ The object to format.
501
+ indent_level
502
+ Current indentation level.
503
+
504
+ Returns
505
+ -------
506
+ unknown
507
+ Formatted JSON string.
508
+ """
509
+ indent = " " * 4 * indent_level
510
+ if isinstance(obj, dict):
511
+ items = []
512
+ for key, value in obj.items():
513
+ items.append(f'"{key}": {_format_json(value, indent_level + 1)}')
514
+ line = "{" + ", ".join(items) + "}"
515
+ if len(line) <= max_line_length:
516
+ return line
517
+ else:
518
+ return "{\n" + ",\n".join([f"{indent} {item}" for item in items]) + "\n" + indent + "}"
519
+ elif isinstance(obj, list):
520
+ items = [_format_json(item, indent_level + 1) for item in obj]
521
+ line = "[" + ", ".join(items) + "]"
522
+ if len(line) <= max_line_length:
523
+ return line
524
+ else:
525
+ return "[\n" + ",\n".join([f"{indent} {item}" for item in items]) + "\n" + indent + "]"
526
+ else:
527
+ return json.dumps(obj, default=default)
528
+
529
+ return _format_json(obj)
@@ -146,6 +146,14 @@ class Upload(Transfer):
146
146
  return os.path.getsize(local_path)
147
147
 
148
148
  def transfer_file(self, source, target, overwrite, resume, verbosity, config=None):
149
+ try:
150
+ return self._transfer_file(source, target, overwrite, resume, verbosity, config=config)
151
+ except Exception as e:
152
+ LOGGER.exception(f"Error transferring {source} to {target}")
153
+ LOGGER.error(e)
154
+ raise
155
+
156
+ def _transfer_file(self, source, target, overwrite, resume, verbosity, config=None):
149
157
 
150
158
  from botocore.exceptions import ClientError
151
159
 
@@ -208,6 +216,14 @@ class Download(Transfer):
208
216
  return s3_object["Size"]
209
217
 
210
218
  def transfer_file(self, source, target, overwrite, resume, verbosity, config=None):
219
+ try:
220
+ return self._transfer_file(source, target, overwrite, resume, verbosity, config=config)
221
+ except Exception as e:
222
+ LOGGER.exception(f"Error transferring {source} to {target}")
223
+ LOGGER.error(e)
224
+ raise
225
+
226
+ def _transfer_file(self, source, target, overwrite, resume, verbosity, config=None):
211
227
  # from boto3.s3.transfer import TransferConfig
212
228
 
213
229
  _, _, bucket, key = source.split("/", 3)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: anemoi-utils
3
- Version: 0.3.6
3
+ Version: 0.3.8
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,13 +30,14 @@ 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
36
37
  src/anemoi/utils/text.py
37
38
  src/anemoi/utils/timer.py
38
39
  src/anemoi/utils/commands/__init__.py
39
- src/anemoi/utils/commands/checkpoint.py
40
+ src/anemoi/utils/commands/config.py
40
41
  src/anemoi/utils/mars/__init__.py
41
42
  src/anemoi/utils/mars/mars.yaml
42
43
  src/anemoi_utils.egg-info/PKG-INFO
@@ -1,61 +0,0 @@
1
- #!/usr/bin/env python
2
- # (C) Copyright 2024 ECMWF.
3
- #
4
- # This software is licensed under the terms of the Apache Licence Version 2.0
5
- # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
6
- # In applying this licence, ECMWF does not waive the privileges and immunities
7
- # granted to it by virtue of its status as an intergovernmental organisation
8
- # nor does it submit to any jurisdiction.
9
- #
10
-
11
-
12
- import json
13
-
14
- from . import Command
15
-
16
-
17
- def visit(x, path, name, value):
18
- if isinstance(x, dict):
19
- for k, v in x.items():
20
- if k == name:
21
- print(".".join(path), k, v)
22
-
23
- if v == value:
24
- print(".".join(path), k, v)
25
-
26
- path.append(k)
27
- visit(v, path, name, value)
28
- path.pop()
29
-
30
- if isinstance(x, list):
31
- for i, v in enumerate(x):
32
- path.append(str(i))
33
- visit(v, path, name, value)
34
- path.pop()
35
-
36
-
37
- class Checkpoint(Command):
38
-
39
- def add_arguments(self, command_parser):
40
- command_parser.add_argument("path", help="Path to the checkpoint.")
41
- command_parser.add_argument("--name", help="Search for a specific name.")
42
- command_parser.add_argument("--value", help="Search for a specific value.")
43
-
44
- def run(self, args):
45
- from anemoi.utils.checkpoints import load_metadata
46
-
47
- checkpoint = load_metadata(args.path, "*.json")
48
-
49
- if args.name or args.value:
50
- visit(
51
- checkpoint,
52
- [],
53
- args.name if args.name is not None else object(),
54
- args.value if args.value is not None else object(),
55
- )
56
- return
57
-
58
- print(json.dumps(checkpoint, sort_keys=True, indent=4))
59
-
60
-
61
- command = Checkpoint
@@ -1,142 +0,0 @@
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 json
10
- import logging
11
- import os
12
- import threading
13
-
14
- import yaml
15
-
16
- try:
17
- import tomllib # Only available since 3.11
18
- except ImportError:
19
- import tomli as tomllib
20
-
21
-
22
- LOG = logging.getLogger(__name__)
23
-
24
-
25
- class DotDict(dict):
26
- """A dictionary that allows access to its keys as attributes.
27
-
28
- >>> d = DotDict({"a": 1, "b": {"c": 2}})
29
- >>> d.a
30
- 1
31
- >>> d.b.c
32
- 2
33
- >>> d.b = 3
34
- >>> d.b
35
- 3
36
-
37
- The class is recursive, so nested dictionaries are also DotDicts.
38
-
39
- The DotDict class has the same constructor as the dict class.
40
-
41
- >>> d = DotDict(a=1, b=2)
42
-
43
- """
44
-
45
- def __init__(self, *args, **kwargs):
46
- super().__init__(*args, **kwargs)
47
-
48
- for k, v in self.items():
49
- if isinstance(v, dict):
50
- self[k] = DotDict(v)
51
-
52
- if isinstance(v, list):
53
- self[k] = [DotDict(i) if isinstance(i, dict) else i for i in v]
54
-
55
- if isinstance(v, tuple):
56
- self[k] = [DotDict(i) if isinstance(i, dict) else i for i in v]
57
-
58
- @classmethod
59
- def from_file(cls, path: str):
60
- _, ext = os.path.splitext(path)
61
- if ext == ".yaml" or ext == ".yml":
62
- return cls.from_yaml_file(path)
63
- elif ext == ".json":
64
- return cls.from_json_file(path)
65
- elif ext == ".toml":
66
- return cls.from_toml_file(path)
67
- else:
68
- raise ValueError(f"Unknown file extension {ext}")
69
-
70
- @classmethod
71
- def from_yaml_file(cls, path: str):
72
- with open(path, "r") as file:
73
- data = yaml.safe_load(file)
74
-
75
- return cls(data)
76
-
77
- @classmethod
78
- def from_json_file(cls, path: str):
79
- with open(path, "r") as file:
80
- data = json.load(file)
81
-
82
- return cls(data)
83
-
84
- @classmethod
85
- def from_toml_file(cls, path: str):
86
- with open(path, "r") as file:
87
- data = tomllib.load(file)
88
- return cls(data)
89
-
90
- def __getattr__(self, attr):
91
- try:
92
- return self[attr]
93
- except KeyError:
94
- raise AttributeError(attr)
95
-
96
- def __setattr__(self, attr, value):
97
- if isinstance(value, dict):
98
- value = DotDict(value)
99
- self[attr] = value
100
-
101
- def __repr__(self) -> str:
102
- return f"DotDict({super().__repr__()})"
103
-
104
-
105
- CONFIG = None
106
- CONFIG_LOCK = threading.Lock()
107
-
108
-
109
- def _load_config():
110
- global CONFIG
111
- if CONFIG is not None:
112
- return CONFIG
113
-
114
- conf = os.path.expanduser("~/.anemoi.toml")
115
-
116
- if os.path.exists(conf):
117
-
118
- with open(conf, "rb") as f:
119
- CONFIG = tomllib.load(f)
120
- else:
121
- CONFIG = {}
122
-
123
- return DotDict(CONFIG)
124
-
125
-
126
- def load_config():
127
- """Load the configuration from `~/.anemoi.toml`.
128
-
129
- Returns
130
- -------
131
- DotDict
132
- The configuration
133
- """
134
- with CONFIG_LOCK:
135
- return _load_config()
136
-
137
-
138
- def check_config_mode():
139
- conf = os.path.expanduser("~/.anemoi.toml")
140
- mode = os.stat(conf).st_mode
141
- if mode & 0o777 != 0o600:
142
- raise SystemError(f"Configuration file {conf} is not secure. " "Please run `chmod 600 ~/.anemoi.toml`.")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes