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.

Files changed (37) hide show
  1. anemoi/utils/__init__.py +1 -0
  2. anemoi/utils/__main__.py +12 -2
  3. anemoi/utils/_version.py +9 -4
  4. anemoi/utils/caching.py +138 -13
  5. anemoi/utils/checkpoints.py +81 -13
  6. anemoi/utils/cli.py +83 -7
  7. anemoi/utils/commands/__init__.py +4 -0
  8. anemoi/utils/commands/config.py +19 -2
  9. anemoi/utils/commands/requests.py +18 -2
  10. anemoi/utils/compatibility.py +6 -5
  11. anemoi/utils/config.py +254 -23
  12. anemoi/utils/dates.py +204 -50
  13. anemoi/utils/devtools.py +68 -7
  14. anemoi/utils/grib.py +30 -9
  15. anemoi/utils/grids.py +85 -8
  16. anemoi/utils/hindcasts.py +25 -8
  17. anemoi/utils/humanize.py +357 -52
  18. anemoi/utils/logs.py +31 -3
  19. anemoi/utils/mars/__init__.py +46 -12
  20. anemoi/utils/mars/requests.py +15 -1
  21. anemoi/utils/provenance.py +189 -32
  22. anemoi/utils/registry.py +234 -44
  23. anemoi/utils/remote/__init__.py +386 -38
  24. anemoi/utils/remote/s3.py +252 -29
  25. anemoi/utils/remote/ssh.py +140 -8
  26. anemoi/utils/s3.py +77 -4
  27. anemoi/utils/sanitise.py +52 -7
  28. anemoi/utils/testing.py +182 -0
  29. anemoi/utils/text.py +218 -54
  30. anemoi/utils/timer.py +91 -15
  31. {anemoi_utils-0.4.12.dist-info → anemoi_utils-0.4.14.dist-info}/METADATA +8 -4
  32. anemoi_utils-0.4.14.dist-info/RECORD +38 -0
  33. {anemoi_utils-0.4.12.dist-info → anemoi_utils-0.4.14.dist-info}/WHEEL +1 -1
  34. anemoi_utils-0.4.12.dist-info/RECORD +0 -37
  35. {anemoi_utils-0.4.12.dist-info → anemoi_utils-0.4.14.dist-info}/entry_points.txt +0 -0
  36. {anemoi_utils-0.4.12.dist-info → anemoi_utils-0.4.14.dist-info/licenses}/LICENSE +0 -0
  37. {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
- def __init__(self, package, key="_type"):
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.registered = {}
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(self, name: str, factory: callable = None):
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
- self.registered[name] = factory
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
- # def registered(self, name: str):
59
- # return name in self.registered
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
- LOG.warning(f"Error loading filter '{self.package}.{name}'", exc_info=True)
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
- def lookup(self, name: str, *, return_none=False) -> callable:
212
+ raise ValueError(f"Cannot find '{name}' in {self.package}")
69
213
 
70
- # print('✅✅✅✅✅✅✅✅✅✅✅✅✅', name, self.registered)
71
- if name in self.registered:
72
- return self.registered[name]
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
- entrypoint_group = f"anemoi.{self.kind}"
94
- for entry_point in entrypoints.get_group_all(entrypoint_group):
95
- if entry_point.name == name:
96
- if name in self.registered:
97
- LOG.warning(
98
- f"Overwriting builtin '{name}' from {self.package} with plugin '{entry_point.module_name}'"
99
- )
100
- self.registered[name] = entry_point.load()
101
-
102
- if name not in self.registered:
103
- if return_none:
104
- return None
105
-
106
- for e in self.registered:
107
- LOG.info(f"Registered: {e}")
108
-
109
- raise ValueError(f"Cannot load '{name}' from {self.package}")
110
-
111
- return self.registered[name]
112
-
113
- def create(self, name: str, *args, **kwargs):
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
- # def __call__(self, name: str, *args, **kwargs):
118
- # return self.create(name, *args, **kwargs)
119
-
120
- def from_config(self, config, *args, **kwargs):
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