anemoi-utils 0.4.17__py3-none-any.whl → 0.4.19__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.17'
21
- __version_tuple__ = version_tuple = (0, 4, 17)
20
+ __version__ = version = '0.4.19'
21
+ __version_tuple__ = version_tuple = (0, 4, 19)
anemoi/utils/registry.py CHANGED
@@ -17,8 +17,10 @@ from functools import cached_property
17
17
  from typing import Any
18
18
  from typing import Callable
19
19
  from typing import Dict
20
+ from typing import Generic
20
21
  from typing import List
21
22
  from typing import Optional
23
+ from typing import TypeVar
22
24
  from typing import Union
23
25
 
24
26
  import entrypoints
@@ -78,8 +80,10 @@ class Error:
78
80
 
79
81
  _BY_KIND = {}
80
82
 
83
+ T = TypeVar("T")
81
84
 
82
- class Registry:
85
+
86
+ class Registry(Generic[T]):
83
87
  """A registry of factories.
84
88
 
85
89
  Parameters
@@ -287,7 +291,7 @@ class Registry:
287
291
 
288
292
  return sorted(self.factories.keys())
289
293
 
290
- def create(self, name: str, *args: Any, **kwargs: Any) -> Any:
294
+ def create(self, name: str, *args: Any, **kwargs: Any) -> T:
291
295
  """Create an instance using a factory.
292
296
 
293
297
  Parameters
@@ -310,7 +314,7 @@ class Registry:
310
314
  factory = self.lookup(name)
311
315
  return factory(*args, **kwargs)
312
316
 
313
- def from_config(self, config: Union[str, Dict[str, Any]], *args: Any, **kwargs: Any) -> Any:
317
+ def from_config(self, config: Union[str, Dict[str, Any]], *args: Any, **kwargs: Any) -> T:
314
318
  """Create an instance from a configuration.
315
319
 
316
320
  Parameters
anemoi/utils/rules.py ADDED
@@ -0,0 +1,218 @@
1
+ # (C) Copyright 2025 Anemoi contributors.
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
+ #
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
+ from typing import Any
11
+ from typing import Dict
12
+ from typing import List
13
+ from typing import Mapping
14
+ from typing import Optional
15
+ from typing import Union
16
+
17
+
18
+ class Rule:
19
+
20
+ def __init__(self, match: Dict[str, Any], result: Any):
21
+ """Initialize a Rule object.
22
+
23
+ Parameters
24
+ ----------
25
+ match : Dict[str, Any]
26
+ A dictionary defining the conditions for the rule to match.
27
+ result : Any
28
+ The result to return if the rule matches.
29
+ """
30
+ self._match = match
31
+ self._result = result
32
+
33
+ def match(self, obj: Mapping[str, Any]) -> bool:
34
+ """Check if the rule matches the given object.
35
+
36
+ Parameters
37
+ ----------
38
+ obj : Mapping[str, Any]
39
+ The object to check against the rule's conditions.
40
+
41
+ Returns
42
+ -------
43
+ bool
44
+ True if the rule matches, False otherwise.
45
+ """
46
+ for key, value in self._match.items():
47
+ if key not in obj or obj[key] != value:
48
+ return False
49
+
50
+ return True
51
+
52
+ @property
53
+ def result(self) -> Any:
54
+ return self._result
55
+
56
+ @property
57
+ def condition(self) -> Dict[str, Any]:
58
+ return self._match
59
+
60
+
61
+ class RuleSet:
62
+
63
+ def __init__(self, rules: List[Union[Rule, Dict[str, Any], List[Any]]]):
64
+ """Initialize a RuleSet object.
65
+
66
+ Parameters
67
+ ----------
68
+ rules : List[Union[Rule, Dict[str, Any], List[Any]]]
69
+ A list of rules, where each rule can be a Rule object, a dictionary with
70
+ 'match' and 'result' keys, or a list with two elements (match and result).
71
+ """
72
+ assert isinstance(rules, list), "rules must be a list"
73
+
74
+ self.rules: List[Rule] = []
75
+
76
+ for rule in rules:
77
+ if isinstance(rule, Rule):
78
+ self.rules.append(rule)
79
+ continue
80
+
81
+ if isinstance(rule, dict):
82
+
83
+ assert len(rule) == 2, "Rule dictionary must contain exactly two key-value pair."
84
+
85
+ match = rule.get("match")
86
+ if match is None:
87
+ raise ValueError("Rule dictionary must contain a 'match' key.")
88
+
89
+ result = rule.get("result")
90
+ if result is None:
91
+ raise ValueError("Rule dictionary must contain a 'result' key.")
92
+
93
+ self.rules.append(Rule(match, result))
94
+ continue
95
+
96
+ if isinstance(rule, list):
97
+ assert len(rule) == 2, "Rule list must contain exactly two elements."
98
+ match = rule[0]
99
+ result = rule[1]
100
+ self.rules.append(Rule(match, result))
101
+ continue
102
+
103
+ raise ValueError(
104
+ "Rule must be either a Rule object, a dictionary with 'match' and 'result' keys, or a list with two elements."
105
+ )
106
+
107
+ @classmethod
108
+ def from_list(cls, rules: List[Any]) -> "RuleSet":
109
+ """Create a RuleSet from a list of rules.
110
+
111
+ Parameters
112
+ ----------
113
+ rules : List[Any]
114
+ A list of rules to initialize the RuleSet.
115
+
116
+ Returns
117
+ -------
118
+ RuleSet
119
+ A new RuleSet object.
120
+ """
121
+ return cls(rules)
122
+
123
+ @classmethod
124
+ def from_files(cls, path: str) -> "RuleSet":
125
+ """Create a RuleSet from a file.
126
+
127
+ Parameters
128
+ ----------
129
+ path : str
130
+ The path to the file containing the rules. Supported formats are .json and .yaml/.yml.
131
+
132
+ Returns
133
+ -------
134
+ RuleSet
135
+ A new RuleSet object.
136
+
137
+ Raises
138
+ ------
139
+ ValueError
140
+ If the file format is unsupported.
141
+ """
142
+ if path.endswith(".json"):
143
+ import json
144
+
145
+ with open(path, "r") as f:
146
+ return cls.from_list(json.load(f))
147
+
148
+ if path.endswith(".yaml") or path.endswith(".yml"):
149
+ import yaml
150
+
151
+ with open(path, "r") as f:
152
+ return cls.from_list(yaml.safe_load(f))
153
+
154
+ raise ValueError("Unsupported file format. Supported formats are .json and .yaml/.yml.")
155
+
156
+ @classmethod
157
+ def from_any(cls, rules: Union[str, List[Any]]) -> "RuleSet":
158
+ """Create a RuleSet from a list or a file path.
159
+
160
+ Parameters
161
+ ----------
162
+ rules : Union[str, List[Any]]
163
+ The rules to initialize the RuleSet, either as a list or a file path.
164
+
165
+ Returns
166
+ -------
167
+ RuleSet
168
+ A new RuleSet object.
169
+
170
+ Raises
171
+ ------
172
+ ValueError
173
+ If the rules format is unsupported.
174
+ """
175
+ if isinstance(rules, str):
176
+ return cls.from_files(rules)
177
+
178
+ if isinstance(rules, list):
179
+ return cls.from_list(rules)
180
+
181
+ raise ValueError("Unsupported rules format. Must be a list or a file path.")
182
+
183
+ def match(self, obj: Mapping[str, Any], strategy: str = "first-match") -> Optional[Rule]:
184
+ """Match an object against the rules in the RuleSet.
185
+
186
+ Parameters
187
+ ----------
188
+ obj : Mapping[str, Any]
189
+ The object to match against the rules.
190
+ strategy : str, optional
191
+ The matching strategy to use. Currently, only 'first-match' is supported.
192
+
193
+ Returns
194
+ -------
195
+ Optional[Rule]
196
+ The first matching rule, or None if no match is found.
197
+
198
+ Raises
199
+ ------
200
+ AssertionError
201
+ If an unsupported strategy is provided.
202
+ """
203
+ assert strategy == "first-match", "Only 'first-match' strategy is supported for now."
204
+ for rule in self.rules:
205
+ if rule.match(obj):
206
+ return rule
207
+
208
+ return None
209
+
210
+ def __iter__(self) -> iter:
211
+ """Return an iterator over the rules in the RuleSet.
212
+
213
+ Returns
214
+ -------
215
+ iter
216
+ An iterator over the Rule objects in the RuleSet.
217
+ """
218
+ return iter(self.rules)
anemoi/utils/testing.py CHANGED
@@ -13,7 +13,10 @@ import os
13
13
  import shutil
14
14
  import tempfile
15
15
  import threading
16
+ import warnings
17
+ from functools import lru_cache
16
18
 
19
+ import pytest
17
20
  from multiurl import download
18
21
 
19
22
  LOG = logging.getLogger(__name__)
@@ -98,6 +101,9 @@ def get_test_data(path: str, gzipped=False) -> str:
98
101
  """
99
102
  _check_path(path)
100
103
 
104
+ if _offline():
105
+ raise RuntimeError("Offline mode: cannot download test data, add @pytest.mark.skipif(not offline(),...)")
106
+
101
107
  target = os.path.normpath(os.path.join(_temporary_directory(), path))
102
108
  with lock:
103
109
  if os.path.exists(target):
@@ -174,9 +180,84 @@ def packages_installed(*names) -> bool:
174
180
  Flag indicating if all the packages are installed."
175
181
  """
176
182
 
183
+ warnings.warn(
184
+ "The 'packages_installed' function is deprecated. Use '@skip_if_missing' instead.",
185
+ DeprecationWarning,
186
+ stacklevel=2,
187
+ )
188
+
177
189
  for name in names:
178
190
  try:
179
191
  __import__(name)
180
192
  except ImportError:
181
193
  return False
182
194
  return True
195
+
196
+
197
+ def _missing_packages(*names) -> list[str]:
198
+ """Check if the given packages are missing.
199
+
200
+ Use this function to check if the required packages are missing before running tests.
201
+
202
+ >>> @pytest.mark.skipif(missing_packages("foo", "bar"), reason="Packages 'foo' and 'bar' are not installed")
203
+ >>> def test_foo_bar() -> None:
204
+ >>> ...
205
+
206
+ Parameters
207
+ ----------
208
+ names : str
209
+ The names of the packages to check.
210
+
211
+ Returns
212
+ -------
213
+ list[str]:
214
+ List of missing packages.
215
+ """
216
+
217
+ missing = []
218
+ for name in names:
219
+ try:
220
+ __import__(name)
221
+ except ImportError:
222
+ missing.append(name)
223
+ return missing
224
+
225
+
226
+ def _run_slow_tests() -> bool:
227
+ """Check if the SLOW_TESTS environment variable is set.
228
+
229
+ Returns
230
+ -------
231
+ bool
232
+ True if the SLOW_TESTS environment variable is set, False otherwise.
233
+ """
234
+ return int(os.environ.get("SLOW_TESTS", 0))
235
+
236
+
237
+ @lru_cache(maxsize=None)
238
+ def _offline() -> bool:
239
+ """Check if we are offline."""
240
+
241
+ import socket
242
+
243
+ try:
244
+ socket.create_connection(("anemoi.ecmwf.int", 443), timeout=5)
245
+ except OSError:
246
+ return True
247
+
248
+ return False
249
+
250
+
251
+ skip_if_offline = pytest.mark.skipif(_offline(), reason="No internet connection")
252
+ skip_slow_tests = pytest.mark.skipif(not _run_slow_tests(), reason="Skipping slow tests")
253
+
254
+
255
+ def skip_missing_packages(*names):
256
+ missing = _missing_packages(*names)
257
+ if len(missing) == 0:
258
+ return lambda f: f
259
+
260
+ if len(missing) == 1:
261
+ return pytest.mark.skipif(True, reason=f"Package {missing[0]} is not installed")
262
+
263
+ return pytest.mark.skipif(True, reason=f"Packages {', '.join(missing)} are not installed")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anemoi-utils
3
- Version: 0.4.17
3
+ Version: 0.4.19
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
@@ -237,6 +237,7 @@ Requires-Dist: anemoi-utils[grib,provenance,s3,text]; extra == "all"
237
237
  Provides-Extra: dev
238
238
  Requires-Dist: anemoi-utils[all,docs,tests]; extra == "dev"
239
239
  Provides-Extra: docs
240
+ Requires-Dist: anemoi-utils[all]; extra == "docs"
240
241
  Requires-Dist: nbsphinx; extra == "docs"
241
242
  Requires-Dist: pandoc; extra == "docs"
242
243
  Requires-Dist: requests; extra == "docs"
@@ -1,6 +1,6 @@
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=UwtfX--7I9I_2cUoyH0-NALXkddhkqIyGBlxSkUZCdU,513
3
+ anemoi/utils/_version.py,sha256=ckoIBbD6SbpUXLLR-S_0NN-a9fgpE7KOAJuuoR3ua9s,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
@@ -14,11 +14,12 @@ anemoi/utils/hindcasts.py,sha256=iYVIxSNFL2HJcc_k1abCFLkpJFGHT8WKRIR4wcAwA3s,214
14
14
  anemoi/utils/humanize.py,sha256=pjnFJAKHbEAOfcvn8c48kt-8eFy6FGW_U2ruJvfamrA,25189
15
15
  anemoi/utils/logs.py,sha256=naTgrmPwWHD4eekFttXftS4gtcAGYHpCqG4iwYprNDA,1804
16
16
  anemoi/utils/provenance.py,sha256=xC6mTstF7f_asqtPSrulC7c34xjOSuAxWhkwc3yKhHg,14629
17
- anemoi/utils/registry.py,sha256=vIaHMT66m0_w8lrY3u5GmT70tIRNyA_Z_p_T7pTFx_k,9637
17
+ anemoi/utils/registry.py,sha256=e3nOIRyMYQ-mpEvaHAv5tuvMYNbkJ5yz94ns7BnvkjM,9717
18
+ anemoi/utils/rules.py,sha256=xYCiUV_HXTGFe93diqMLQsMJCWGi5umd_bWEeYP8XFY,6318
18
19
  anemoi/utils/s3.py,sha256=xMT48kbcelcjjqsaU567WI3oZ5eqo88Rlgyx5ECszAU,4074
19
20
  anemoi/utils/sanitise.py,sha256=ZYGdSX6qihQANr3pHZjbKnoapnzP1KcrWdW1Ul1mOGk,3668
20
21
  anemoi/utils/sanitize.py,sha256=43ZKDcfVpeXSsJ9TFEc9aZnD6oe2cUh151XnDspM98M,462
21
- anemoi/utils/testing.py,sha256=N1y4dfZLE9zqOhIR3o-933fdAdd9BxDvjcJx7SwFC9A,4803
22
+ anemoi/utils/testing.py,sha256=2H9yxsZriCF_juVX0zYsUCBwA9XC20OfSRZvFkRH7jY,6873
22
23
  anemoi/utils/text.py,sha256=HkzIvi24obDceFLpJEwBJ9PmPrJUkQN2TrElJ-A87gU,14441
23
24
  anemoi/utils/timer.py,sha256=_leKMYza2faM7JKlGE7LCNy13rbdPnwaCF7PSrI_NmI,3895
24
25
  anemoi/utils/commands/__init__.py,sha256=5u_6EwdqYczIAgJfCwRSyQAYFEqh2ZuHHT57g9g7sdI,808
@@ -30,9 +31,9 @@ anemoi/utils/mars/requests.py,sha256=VFMHBVAAl0_2lOcMBa1lvaKHctN0lDJsI6_U4BucGew
30
31
  anemoi/utils/remote/__init__.py,sha256=swPWHQoh-B6Xq9R489tPw0FykMue7f-bJ8enneFYSYE,20776
31
32
  anemoi/utils/remote/s3.py,sha256=spQ8l0rwQjLZh9dZu5cOsYIvNwKihQfCJ6YsFYegeqI,17339
32
33
  anemoi/utils/remote/ssh.py,sha256=xNtsawh8okytCKRehkRCVExbHZj-CRUQNormEHglfuw,8088
33
- anemoi_utils-0.4.17.dist-info/licenses/LICENSE,sha256=8HznKF1Vi2IvfLsKNE5A2iVyiri3pRjRPvPC9kxs6qk,11354
34
- anemoi_utils-0.4.17.dist-info/METADATA,sha256=XkTQFxHQ9BRkUl77BycvPb3F6yPGxqS9x5w-aRQlLYc,15360
35
- anemoi_utils-0.4.17.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
36
- anemoi_utils-0.4.17.dist-info/entry_points.txt,sha256=LENOkn88xzFQo-V59AKoA_F_cfYQTJYtrNTtf37YgHY,60
37
- anemoi_utils-0.4.17.dist-info/top_level.txt,sha256=DYn8VPs-fNwr7fNH9XIBqeXIwiYYd2E2k5-dUFFqUz0,7
38
- anemoi_utils-0.4.17.dist-info/RECORD,,
34
+ anemoi_utils-0.4.19.dist-info/licenses/LICENSE,sha256=8HznKF1Vi2IvfLsKNE5A2iVyiri3pRjRPvPC9kxs6qk,11354
35
+ anemoi_utils-0.4.19.dist-info/METADATA,sha256=IeSKCPc77w2aDSaRudxLPmy70LE9-bMwqNq15lZyT24,15410
36
+ anemoi_utils-0.4.19.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
37
+ anemoi_utils-0.4.19.dist-info/entry_points.txt,sha256=LENOkn88xzFQo-V59AKoA_F_cfYQTJYtrNTtf37YgHY,60
38
+ anemoi_utils-0.4.19.dist-info/top_level.txt,sha256=DYn8VPs-fNwr7fNH9XIBqeXIwiYYd2E2k5-dUFFqUz0,7
39
+ anemoi_utils-0.4.19.dist-info/RECORD,,