anemoi-utils 0.4.20__py3-none-any.whl → 0.4.22__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
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.4.20'
21
- __version_tuple__ = version_tuple = (0, 4, 20)
20
+ __version__ = version = '0.4.22'
21
+ __version_tuple__ = version_tuple = (0, 4, 22)
anemoi/utils/config.py CHANGED
@@ -10,6 +10,7 @@
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ import contextlib
13
14
  import json
14
15
  import logging
15
16
  import os
@@ -239,6 +240,7 @@ CONFIG = {}
239
240
  CHECKED = {}
240
241
  CONFIG_LOCK = threading.RLock()
241
242
  QUIET = False
243
+ CONFIG_PATCH = None
242
244
 
243
245
 
244
246
  def _find(config: Union[dict, list], what: str, result: list = None) -> list:
@@ -550,7 +552,10 @@ def load_config(
550
552
  """
551
553
 
552
554
  with CONFIG_LOCK:
553
- return _load_config(name, secrets, defaults)
555
+ config = _load_config(name, secrets, defaults)
556
+ if CONFIG_PATCH is not None:
557
+ config = CONFIG_PATCH(config)
558
+ return config
554
559
 
555
560
 
556
561
  def load_raw_config(name: str, default: Any = None) -> Union[DotDict, str]:
@@ -668,3 +673,21 @@ def merge_configs(*configs: dict) -> dict:
668
673
  _merge_dicts(result, config)
669
674
 
670
675
  return result
676
+
677
+
678
+ @contextlib.contextmanager
679
+ def temporary_config(tmp: dict) -> None:
680
+
681
+ global CONFIG_PATCH
682
+
683
+ def patch_config(config: dict) -> dict:
684
+ return merge_configs(config, tmp)
685
+
686
+ with CONFIG_LOCK:
687
+
688
+ CONFIG_PATCH = patch_config
689
+
690
+ try:
691
+ yield
692
+ finally:
693
+ CONFIG_PATCH = None
anemoi/utils/rules.py CHANGED
@@ -51,10 +51,12 @@ class Rule:
51
51
 
52
52
  @property
53
53
  def result(self) -> Any:
54
+ """The result associated with the rule."""
54
55
  return self._result
55
56
 
56
57
  @property
57
58
  def condition(self) -> Dict[str, Any]:
59
+ """The conditions that define the rule."""
58
60
  return self._match
59
61
 
60
62
 
@@ -216,3 +218,28 @@ class RuleSet:
216
218
  An iterator over the Rule objects in the RuleSet.
217
219
  """
218
220
  return iter(self.rules)
221
+
222
+ def __len__(self) -> int:
223
+ """Return the number of rules in the RuleSet.
224
+
225
+ Returns
226
+ -------
227
+ int
228
+ The number of rules in the RuleSet.
229
+ """
230
+ return len(self.rules)
231
+
232
+ def __getitem__(self, index: int) -> Rule:
233
+ """Retrieve a rule by its index.
234
+
235
+ Parameters
236
+ ----------
237
+ index : int
238
+ The index of the rule to retrieve.
239
+
240
+ Returns
241
+ -------
242
+ Rule
243
+ The rule at the specified index.
244
+ """
245
+ return self.rules[index]
@@ -0,0 +1,20 @@
1
+ # (C) Copyright 2025 ECMWF.
2
+ #
3
+ # This software is licensed under the terms of the Apache Licence Version 2.0
4
+ # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
5
+ # In applying this licence, ECMWF does not waive the privileges and immunities
6
+ # granted to it by virtue of its status as an intergovernmental organisation
7
+ # nor does it submit to any jurisdiction.
8
+
9
+ from pydantic import BaseModel as PydanticBaseModel
10
+
11
+
12
+ class BaseModel(PydanticBaseModel):
13
+ class Config:
14
+ """Pydantic BaseModel configuration."""
15
+
16
+ use_attribute_docstrings = True
17
+ use_enum_values = True
18
+ validate_assignment = True
19
+ validate_default = True
20
+ extra = "forbid"
@@ -0,0 +1,54 @@
1
+ # (C) Copyright 2025 ECMWF.
2
+ #
3
+ # This software is licensed under the terms of the Apache Licence Version 2.0
4
+ # which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
5
+ # In applying this licence, ECMWF does not waive the privileges and immunities
6
+ # granted to it by virtue of its status as an intergovernmental organisation
7
+ # nor does it submit to any jurisdiction.
8
+
9
+ from collections.abc import Iterator
10
+ from typing import Any
11
+
12
+ from pydantic import BaseModel as PydanticBaseModel
13
+ from pydantic import ValidationError
14
+ from pydantic_core import ErrorDetails
15
+
16
+ CUSTOM_MESSAGES = {
17
+ "missing": "A config entry seems to be missing. If not please check for any typos.",
18
+ "extra_forbidden": "Extra entries in the config are forebidden. Please check for typos.",
19
+ }
20
+
21
+
22
+ def convert_errors(e: ValidationError, custom_messages: dict[str, str]) -> list[ErrorDetails]:
23
+ new_errors: list[ErrorDetails] = []
24
+ for error in e.errors():
25
+ custom_message = custom_messages.get(error["type"])
26
+
27
+ if custom_message:
28
+
29
+ ctx = error.get("ctx")
30
+ error["msg"] = custom_message.format(**ctx) if ctx else custom_message
31
+ new_errors.append(error)
32
+ return new_errors
33
+
34
+
35
+ class ValidationError(Exception):
36
+ pass
37
+
38
+
39
+ def allowed_values(v: Any, values: list[Any]) -> Any:
40
+ if v not in values:
41
+ msg = {f"Value {v} not in {values}"}
42
+ raise ValidationError(msg)
43
+ return v
44
+
45
+
46
+ def required_fields(model: type[PydanticBaseModel], recursive: bool = False) -> Iterator[str]:
47
+ for name, field in model.model_fields.items():
48
+ if not field.is_required():
49
+ continue
50
+ t = field.annotation
51
+ if recursive and isinstance(t, type) and issubclass(t, PydanticBaseModel):
52
+ yield from required_fields(t, recursive=True)
53
+ else:
54
+ yield name
anemoi/utils/testing.py CHANGED
@@ -68,6 +68,23 @@ def _check_path(path: str) -> None:
68
68
  assert not path.startswith("."), f"Path '{path}' should not start with '.'"
69
69
 
70
70
 
71
+ def _temporary_directory_for_test_data(path: str) -> str:
72
+ """Get the temporary directory for a test dataset.
73
+
74
+ Parameters
75
+ ----------
76
+ path : str
77
+ The relative path to the test data in the object store.
78
+
79
+ Returns
80
+ -------
81
+ str
82
+ The path to the temporary directory.
83
+ """
84
+ _check_path(path)
85
+ return os.path.normpath(os.path.join(_temporary_directory(), path))
86
+
87
+
71
88
  def url_for_test_data(path: str) -> str:
72
89
  """Generate the URL for the test data based on the given path.
73
90
 
@@ -101,12 +118,11 @@ def get_test_data(path: str, gzipped=False) -> str:
101
118
  str
102
119
  The local path to the downloaded test data.
103
120
  """
104
- _check_path(path)
105
121
 
106
122
  if _offline():
107
123
  raise RuntimeError("Offline mode: cannot download test data, add @pytest.mark.skipif(not offline(),...)")
108
124
 
109
- target = os.path.normpath(os.path.join(_temporary_directory(), path))
125
+ target = _temporary_directory_for_test_data(path)
110
126
  with lock:
111
127
  if os.path.exists(target):
112
128
  return target
@@ -153,16 +169,22 @@ def get_test_archive(path: str, extension=".extracted") -> str:
153
169
 
154
170
  with lock:
155
171
 
172
+ target = _temporary_directory_for_test_data(path) + extension
173
+
174
+ if os.path.exists(target):
175
+ return target
176
+
156
177
  archive = get_test_data(path)
157
- target = archive + extension
158
178
 
159
179
  shutil.unpack_archive(archive, os.path.dirname(target) + ".tmp")
160
180
  os.rename(os.path.dirname(target) + ".tmp", target)
161
181
 
182
+ os.remove(archive)
183
+
162
184
  return target
163
185
 
164
186
 
165
- def packages_installed(*names) -> bool:
187
+ def packages_installed(*names: str) -> bool:
166
188
  """Check if all the given packages are installed.
167
189
 
168
190
  Use this function to check if the required packages are installed before running tests.
@@ -196,7 +218,7 @@ def packages_installed(*names) -> bool:
196
218
  return True
197
219
 
198
220
 
199
- def _missing_packages(*names) -> list[str]:
221
+ def _missing_packages(*names: str) -> list[str]:
200
222
  """Check if the given packages are missing.
201
223
 
202
224
  Use this function to check if the required packages are missing before running tests.
@@ -254,7 +276,20 @@ skip_if_offline = pytest.mark.skipif(_offline(), reason="No internet connection"
254
276
  skip_slow_tests = pytest.mark.skipif(not _run_slow_tests(), reason="Skipping slow tests")
255
277
 
256
278
 
257
- def skip_missing_packages(*names):
279
+ def skip_missing_packages(*names: str) -> callable:
280
+ """Skip a test if any of the specified packages are missing.
281
+
282
+ Parameters
283
+ ----------
284
+ names : str
285
+ The names of the packages to check.
286
+
287
+ Returns
288
+ -------
289
+ Callable
290
+ A decorator that skips the test if any of the specified packages are missing.
291
+ """
292
+
258
293
  missing = [f"'{p}'" for p in _missing_packages(*names)]
259
294
 
260
295
  if len(missing) == 0:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anemoi-utils
3
- Version: 0.4.20
3
+ Version: 0.4.22
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
@@ -228,6 +228,7 @@ Requires-Dist: aniso8601
228
228
  Requires-Dist: importlib-metadata; python_version < "3.10"
229
229
  Requires-Dist: multiurl
230
230
  Requires-Dist: numpy
231
+ Requires-Dist: pydantic>=2.9
231
232
  Requires-Dist: python-dateutil
232
233
  Requires-Dist: pyyaml
233
234
  Requires-Dist: tomli; python_version < "3.11"
@@ -1,11 +1,11 @@
1
1
  anemoi/utils/__init__.py,sha256=uVhpF-VjIl_4mMywOVtgTutgsdIsqz-xdkwxeMhzuag,730
2
2
  anemoi/utils/__main__.py,sha256=6LlE4MYrPvqqrykxXh7XMi50UZteUY59NeM8P9Zs2dU,910
3
- anemoi/utils/_version.py,sha256=a0Lwyg5BR-wqrpFfOdiqEhJkQTdIGbZv9WA1aUNeyF0,513
3
+ anemoi/utils/_version.py,sha256=ncPL58_Lzy93I55YUff3cwMUk7Kbpd4bhXC7ujoaqGw,513
4
4
  anemoi/utils/caching.py,sha256=rXbeAmpBcMbbfN4EVblaHWKicsrtx1otER84FEBtz98,6183
5
5
  anemoi/utils/checkpoints.py,sha256=N4WpAZXa4etrpSEKhHqUUtG2-x9w3FJMHcLO-dDAXPY,9600
6
6
  anemoi/utils/cli.py,sha256=IyZfnSw0u0yYnrjOrzvm2RuuKvDk4cVb8pf8BkaChgA,6209
7
7
  anemoi/utils/compatibility.py,sha256=wRBRMmxQP88rNcWiP5gqXliwYQbBv1iCAsDjcCRi5UY,2234
8
- anemoi/utils/config.py,sha256=UtxlkSMhqZR0LAKi19aG6jaaNiSmsXCzE6v2AWHhx5E,16861
8
+ anemoi/utils/config.py,sha256=EEfcSxW2CD6fFOzDtqz_uYlMKuYq4X5QinJW_8GBYj4,17325
9
9
  anemoi/utils/dates.py,sha256=CnY6JOdpk0T-waPEowMRTkcMzxcN0GcjPVtLkwH_byw,17196
10
10
  anemoi/utils/devtools.py,sha256=W3OBu96MkXRIl7Qh1SE5Zd6aB1R0QlnmlrlpBYM0fVY,3527
11
11
  anemoi/utils/grib.py,sha256=201WcxjjAl92Y2HX2kZ2S8Qr5dN-oG7nV-vQLaybzP4,3610
@@ -15,11 +15,11 @@ anemoi/utils/humanize.py,sha256=pjnFJAKHbEAOfcvn8c48kt-8eFy6FGW_U2ruJvfamrA,2518
15
15
  anemoi/utils/logs.py,sha256=naTgrmPwWHD4eekFttXftS4gtcAGYHpCqG4iwYprNDA,1804
16
16
  anemoi/utils/provenance.py,sha256=xC6mTstF7f_asqtPSrulC7c34xjOSuAxWhkwc3yKhHg,14629
17
17
  anemoi/utils/registry.py,sha256=e3nOIRyMYQ-mpEvaHAv5tuvMYNbkJ5yz94ns7BnvkjM,9717
18
- anemoi/utils/rules.py,sha256=xYCiUV_HXTGFe93diqMLQsMJCWGi5umd_bWEeYP8XFY,6318
18
+ anemoi/utils/rules.py,sha256=VspUoPmw7tijrs6l_wl4vDjr_zVQsFjx9ITiBSvxgc8,6972
19
19
  anemoi/utils/s3.py,sha256=xMT48kbcelcjjqsaU567WI3oZ5eqo88Rlgyx5ECszAU,4074
20
20
  anemoi/utils/sanitise.py,sha256=ZYGdSX6qihQANr3pHZjbKnoapnzP1KcrWdW1Ul1mOGk,3668
21
21
  anemoi/utils/sanitize.py,sha256=43ZKDcfVpeXSsJ9TFEc9aZnD6oe2cUh151XnDspM98M,462
22
- anemoi/utils/testing.py,sha256=7wDIpiNIhXhHv5rN-LjgTY9DqMMMVpwPbzvYpWTKfLg,6947
22
+ anemoi/utils/testing.py,sha256=psfHfluNqXa-cXSDF4xD7xIQCVPvBGDNCWWZ-yVh_24,7750
23
23
  anemoi/utils/text.py,sha256=HkzIvi24obDceFLpJEwBJ9PmPrJUkQN2TrElJ-A87gU,14441
24
24
  anemoi/utils/timer.py,sha256=_leKMYza2faM7JKlGE7LCNy13rbdPnwaCF7PSrI_NmI,3895
25
25
  anemoi/utils/commands/__init__.py,sha256=5u_6EwdqYczIAgJfCwRSyQAYFEqh2ZuHHT57g9g7sdI,808
@@ -31,9 +31,11 @@ anemoi/utils/mars/requests.py,sha256=VFMHBVAAl0_2lOcMBa1lvaKHctN0lDJsI6_U4BucGew
31
31
  anemoi/utils/remote/__init__.py,sha256=swPWHQoh-B6Xq9R489tPw0FykMue7f-bJ8enneFYSYE,20776
32
32
  anemoi/utils/remote/s3.py,sha256=spQ8l0rwQjLZh9dZu5cOsYIvNwKihQfCJ6YsFYegeqI,17339
33
33
  anemoi/utils/remote/ssh.py,sha256=xNtsawh8okytCKRehkRCVExbHZj-CRUQNormEHglfuw,8088
34
- anemoi_utils-0.4.20.dist-info/licenses/LICENSE,sha256=8HznKF1Vi2IvfLsKNE5A2iVyiri3pRjRPvPC9kxs6qk,11354
35
- anemoi_utils-0.4.20.dist-info/METADATA,sha256=3fUbrATAsi8f1GCjTqsfLS3ND00wSzfpfjmzOvqVljQ,15410
36
- anemoi_utils-0.4.20.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
37
- anemoi_utils-0.4.20.dist-info/entry_points.txt,sha256=LENOkn88xzFQo-V59AKoA_F_cfYQTJYtrNTtf37YgHY,60
38
- anemoi_utils-0.4.20.dist-info/top_level.txt,sha256=DYn8VPs-fNwr7fNH9XIBqeXIwiYYd2E2k5-dUFFqUz0,7
39
- anemoi_utils-0.4.20.dist-info/RECORD,,
34
+ anemoi/utils/schemas/__init__.py,sha256=nkinKlsPLPXEjfTYQT1mpKC4cvs-14w_zBkDRxakwxw,698
35
+ anemoi/utils/schemas/errors.py,sha256=lgOXzVTYzAE0qWQf3OZ42vCWixv8lilSqLLhzARBmvI,1831
36
+ anemoi_utils-0.4.22.dist-info/licenses/LICENSE,sha256=8HznKF1Vi2IvfLsKNE5A2iVyiri3pRjRPvPC9kxs6qk,11354
37
+ anemoi_utils-0.4.22.dist-info/METADATA,sha256=DOdoppZLABKmpmK5DAgYtEl5BoKs7zMkWmquG6qFfgo,15439
38
+ anemoi_utils-0.4.22.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
39
+ anemoi_utils-0.4.22.dist-info/entry_points.txt,sha256=LENOkn88xzFQo-V59AKoA_F_cfYQTJYtrNTtf37YgHY,60
40
+ anemoi_utils-0.4.22.dist-info/top_level.txt,sha256=DYn8VPs-fNwr7fNH9XIBqeXIwiYYd2E2k5-dUFFqUz0,7
41
+ anemoi_utils-0.4.22.dist-info/RECORD,,