anemoi-utils 0.4.12__py3-none-any.whl → 0.4.14__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/__init__.py +1 -0
- anemoi/utils/__main__.py +12 -2
- anemoi/utils/_version.py +9 -4
- anemoi/utils/caching.py +138 -13
- anemoi/utils/checkpoints.py +81 -13
- anemoi/utils/cli.py +83 -7
- anemoi/utils/commands/__init__.py +4 -0
- anemoi/utils/commands/config.py +19 -2
- anemoi/utils/commands/requests.py +18 -2
- anemoi/utils/compatibility.py +6 -5
- anemoi/utils/config.py +254 -23
- anemoi/utils/dates.py +204 -50
- anemoi/utils/devtools.py +68 -7
- anemoi/utils/grib.py +30 -9
- anemoi/utils/grids.py +85 -8
- anemoi/utils/hindcasts.py +25 -8
- anemoi/utils/humanize.py +357 -52
- anemoi/utils/logs.py +31 -3
- anemoi/utils/mars/__init__.py +46 -12
- anemoi/utils/mars/requests.py +15 -1
- anemoi/utils/provenance.py +189 -32
- anemoi/utils/registry.py +234 -44
- anemoi/utils/remote/__init__.py +386 -38
- anemoi/utils/remote/s3.py +252 -29
- anemoi/utils/remote/ssh.py +140 -8
- anemoi/utils/s3.py +77 -4
- anemoi/utils/sanitise.py +52 -7
- anemoi/utils/testing.py +182 -0
- anemoi/utils/text.py +218 -54
- anemoi/utils/timer.py +91 -15
- {anemoi_utils-0.4.12.dist-info → anemoi_utils-0.4.14.dist-info}/METADATA +8 -4
- anemoi_utils-0.4.14.dist-info/RECORD +38 -0
- {anemoi_utils-0.4.12.dist-info → anemoi_utils-0.4.14.dist-info}/WHEEL +1 -1
- anemoi_utils-0.4.12.dist-info/RECORD +0 -37
- {anemoi_utils-0.4.12.dist-info → anemoi_utils-0.4.14.dist-info}/entry_points.txt +0 -0
- {anemoi_utils-0.4.12.dist-info → anemoi_utils-0.4.14.dist-info/licenses}/LICENSE +0 -0
- {anemoi_utils-0.4.12.dist-info → anemoi_utils-0.4.14.dist-info}/top_level.txt +0 -0
anemoi/utils/registry.py
CHANGED
|
@@ -12,64 +12,209 @@ import importlib
|
|
|
12
12
|
import logging
|
|
13
13
|
import os
|
|
14
14
|
import sys
|
|
15
|
+
import warnings
|
|
16
|
+
from functools import cached_property
|
|
17
|
+
from typing import Any
|
|
18
|
+
from typing import Callable
|
|
19
|
+
from typing import Dict
|
|
20
|
+
from typing import List
|
|
21
|
+
from typing import Optional
|
|
22
|
+
from typing import Union
|
|
15
23
|
|
|
16
24
|
import entrypoints
|
|
17
25
|
|
|
18
26
|
LOG = logging.getLogger(__name__)
|
|
19
27
|
|
|
28
|
+
DEBUG_ANEMOI_REGISTRY = int(os.environ.get("DEBUG_ANEMOI_REGISTRY", "0"))
|
|
29
|
+
|
|
20
30
|
|
|
21
31
|
class Wrapper:
|
|
22
|
-
"""A wrapper for the registry
|
|
32
|
+
"""A wrapper for the registry.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
name : str
|
|
37
|
+
The name of the wrapper.
|
|
38
|
+
registry : Registry
|
|
39
|
+
The registry to wrap.
|
|
40
|
+
"""
|
|
23
41
|
|
|
24
|
-
def __init__(self, name, registry):
|
|
42
|
+
def __init__(self, name: str, registry: "Registry"):
|
|
25
43
|
self.name = name
|
|
26
44
|
self.registry = registry
|
|
27
45
|
|
|
28
|
-
def __call__(self, factory):
|
|
46
|
+
def __call__(self, factory: Callable) -> Callable:
|
|
47
|
+
"""Register a factory with the registry.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
factory : Callable
|
|
52
|
+
The factory to register.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
Callable
|
|
57
|
+
The registered factory.
|
|
58
|
+
"""
|
|
29
59
|
self.registry.register(self.name, factory)
|
|
30
60
|
return factory
|
|
31
61
|
|
|
32
62
|
|
|
63
|
+
class Error:
|
|
64
|
+
"""An error class. Used in place of a plugin that failed to load.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
error : Exception
|
|
69
|
+
The error.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, error: Exception):
|
|
73
|
+
self.error = error
|
|
74
|
+
|
|
75
|
+
def __call__(self, *args, **kwargs):
|
|
76
|
+
raise self.error
|
|
77
|
+
|
|
78
|
+
|
|
33
79
|
_BY_KIND = {}
|
|
34
80
|
|
|
35
81
|
|
|
36
82
|
class Registry:
|
|
37
|
-
"""A registry of factories
|
|
83
|
+
"""A registry of factories.
|
|
38
84
|
|
|
39
|
-
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
package : str
|
|
88
|
+
The package name.
|
|
89
|
+
key : str, optional
|
|
90
|
+
The key to use for the registry, by default "_type".
|
|
91
|
+
"""
|
|
40
92
|
|
|
93
|
+
def __init__(self, package: str, key: str = "_type"):
|
|
41
94
|
self.package = package
|
|
42
|
-
self.
|
|
95
|
+
self.__registered = {}
|
|
96
|
+
self._sources = {}
|
|
43
97
|
self.kind = package.split(".")[-1]
|
|
44
98
|
self.key = key
|
|
45
99
|
_BY_KIND[self.kind] = self
|
|
46
100
|
|
|
47
101
|
@classmethod
|
|
48
|
-
def lookup_kind(cls, kind: str):
|
|
102
|
+
def lookup_kind(cls, kind: str) -> Optional["Registry"]:
|
|
103
|
+
"""Lookup a registry by kind.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
kind : str
|
|
108
|
+
The kind of the registry.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
Registry, optional
|
|
113
|
+
The registry if found, otherwise None.
|
|
114
|
+
"""
|
|
49
115
|
return _BY_KIND.get(kind)
|
|
50
116
|
|
|
51
|
-
def register(
|
|
52
|
-
|
|
117
|
+
def register(
|
|
118
|
+
self, name: str, factory: Optional[Callable] = None, source: Optional[Any] = None
|
|
119
|
+
) -> Optional[Wrapper]:
|
|
120
|
+
"""Register a factory with the registry.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
name : str
|
|
125
|
+
The name of the factory.
|
|
126
|
+
factory : Callable, optional
|
|
127
|
+
The factory to register, by default None.
|
|
128
|
+
source : Any, optional
|
|
129
|
+
The source of the factory, by default None.
|
|
130
|
+
|
|
131
|
+
Returns
|
|
132
|
+
-------
|
|
133
|
+
Wrapper, optional
|
|
134
|
+
A wrapper if the factory is None, otherwise None.
|
|
135
|
+
"""
|
|
53
136
|
if factory is None:
|
|
137
|
+
# This happens when the @register decorator is used
|
|
54
138
|
return Wrapper(name, self)
|
|
55
139
|
|
|
56
|
-
|
|
140
|
+
if source is None:
|
|
141
|
+
source = getattr(factory, "_source") if hasattr(factory, "_source") else factory
|
|
142
|
+
|
|
143
|
+
if name in self.__registered:
|
|
144
|
+
warnings.warn(f"Factory '{name}' is already registered in {self.package}")
|
|
145
|
+
warnings.warn(f"Existing: {self._sources[name]}")
|
|
146
|
+
warnings.warn(f"New: {source}")
|
|
57
147
|
|
|
58
|
-
|
|
59
|
-
|
|
148
|
+
self.__registered[name] = factory
|
|
149
|
+
self._sources[name] = source
|
|
60
150
|
|
|
61
|
-
def _load(self, file):
|
|
151
|
+
def _load(self, file: str) -> None:
|
|
152
|
+
"""Load a module from a file.
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
file : str
|
|
157
|
+
The file to load.
|
|
158
|
+
"""
|
|
62
159
|
name, _ = os.path.splitext(file)
|
|
63
160
|
try:
|
|
64
161
|
importlib.import_module(f".{name}", package=self.package)
|
|
65
|
-
except Exception:
|
|
66
|
-
|
|
162
|
+
except Exception as e:
|
|
163
|
+
if DEBUG_ANEMOI_REGISTRY:
|
|
164
|
+
raise
|
|
165
|
+
self._registered[name] = Error(e)
|
|
166
|
+
|
|
167
|
+
def is_registered(self, name: str) -> bool:
|
|
168
|
+
"""Check if a factory is registered.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
name : str
|
|
173
|
+
The name of the factory.
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
bool
|
|
178
|
+
Whether the factory is registered.
|
|
179
|
+
"""
|
|
180
|
+
ok = name in self.factories
|
|
181
|
+
if not ok:
|
|
182
|
+
LOG.error(f"Cannot find '{name}' in {self.package}")
|
|
183
|
+
for e in self.factories:
|
|
184
|
+
LOG.info(f"Registered: {e} ({self._sources.get(e)})")
|
|
185
|
+
return ok
|
|
186
|
+
|
|
187
|
+
def lookup(self, name: str, *, return_none: bool = False) -> Optional[Callable]:
|
|
188
|
+
"""Lookup a factory by name.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
name : str
|
|
193
|
+
The name of the factory.
|
|
194
|
+
return_none : bool, optional
|
|
195
|
+
Whether to return None if the factory is not found, by default False.
|
|
196
|
+
|
|
197
|
+
Returns
|
|
198
|
+
-------
|
|
199
|
+
Callable, optional
|
|
200
|
+
The factory if found, otherwise None.
|
|
201
|
+
"""
|
|
202
|
+
if return_none:
|
|
203
|
+
return self.factories.get(name)
|
|
204
|
+
|
|
205
|
+
factory = self.factories.get(name)
|
|
206
|
+
if factory is None:
|
|
207
|
+
|
|
208
|
+
LOG.error(f"Cannot find '{name}' in {self.package}")
|
|
209
|
+
for e in self.factories:
|
|
210
|
+
LOG.info(f"Registered: {e} ({self._sources.get(e)})")
|
|
67
211
|
|
|
68
|
-
|
|
212
|
+
raise ValueError(f"Cannot find '{name}' in {self.package}")
|
|
69
213
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
214
|
+
return factory
|
|
215
|
+
|
|
216
|
+
@cached_property
|
|
217
|
+
def factories(self) -> Dict[str, Callable]:
|
|
73
218
|
|
|
74
219
|
directory = sys.modules[self.package].__path__[0]
|
|
75
220
|
|
|
@@ -90,34 +235,79 @@ class Registry:
|
|
|
90
235
|
if file.endswith(".py"):
|
|
91
236
|
self._load(file)
|
|
92
237
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
238
|
+
bits = self.package.split(".")
|
|
239
|
+
# We assume a name like anemoi.datasets.create.sources, with kind = sources
|
|
240
|
+
assert bits[-1] == self.kind, (self.package, self.kind)
|
|
241
|
+
assert len(bits) > 1, self.package
|
|
242
|
+
|
|
243
|
+
groups = []
|
|
244
|
+
middle = bits[1:-1]
|
|
245
|
+
while True:
|
|
246
|
+
group = ".".join([bits[0], *middle, bits[-1]])
|
|
247
|
+
groups.append(group)
|
|
248
|
+
if len(middle) == 0:
|
|
249
|
+
break
|
|
250
|
+
middle.pop()
|
|
251
|
+
|
|
252
|
+
groups.reverse()
|
|
253
|
+
|
|
254
|
+
LOG.debug("Loading plugins from %s", groups)
|
|
255
|
+
|
|
256
|
+
for entrypoint_group in groups:
|
|
257
|
+
for entry_point in entrypoints.get_group_all(entrypoint_group):
|
|
258
|
+
source = entry_point.distro
|
|
259
|
+
try:
|
|
260
|
+
self.register(entry_point.name, entry_point.load(), source=source)
|
|
261
|
+
except Exception as e:
|
|
262
|
+
if DEBUG_ANEMOI_REGISTRY:
|
|
263
|
+
raise
|
|
264
|
+
self.register(entry_point.name, Error(e), source=source)
|
|
265
|
+
|
|
266
|
+
return self.__registered
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def registered(self) -> List[str]:
|
|
270
|
+
"""Get the registered factories."""
|
|
271
|
+
|
|
272
|
+
return sorted(self.factories.keys())
|
|
273
|
+
|
|
274
|
+
def create(self, name: str, *args: Any, **kwargs: Any) -> Any:
|
|
275
|
+
"""Create an instance using a factory.
|
|
276
|
+
|
|
277
|
+
Parameters
|
|
278
|
+
----------
|
|
279
|
+
name : str
|
|
280
|
+
The name of the factory.
|
|
281
|
+
*args : Any
|
|
282
|
+
Positional arguments for the factory.
|
|
283
|
+
**kwargs : Any
|
|
284
|
+
Keyword arguments for the factory.
|
|
285
|
+
|
|
286
|
+
Returns
|
|
287
|
+
-------
|
|
288
|
+
Any
|
|
289
|
+
The created instance.
|
|
290
|
+
"""
|
|
114
291
|
factory = self.lookup(name)
|
|
115
292
|
return factory(*args, **kwargs)
|
|
116
293
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
294
|
+
def from_config(self, config: Union[str, Dict[str, Any]], *args: Any, **kwargs: Any) -> Any:
|
|
295
|
+
"""Create an instance from a configuration.
|
|
296
|
+
|
|
297
|
+
Parameters
|
|
298
|
+
----------
|
|
299
|
+
config : str or dict
|
|
300
|
+
The configuration.
|
|
301
|
+
*args : Any
|
|
302
|
+
Positional arguments for the factory.
|
|
303
|
+
**kwargs : Any
|
|
304
|
+
Keyword arguments for the factory.
|
|
305
|
+
|
|
306
|
+
Returns
|
|
307
|
+
-------
|
|
308
|
+
Any
|
|
309
|
+
The created instance.
|
|
310
|
+
"""
|
|
121
311
|
if isinstance(config, str):
|
|
122
312
|
config = {config: {}}
|
|
123
313
|
|