anemoi-datasets 0.5.26__py3-none-any.whl → 0.5.28__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.
- anemoi/datasets/__init__.py +1 -2
- anemoi/datasets/_version.py +16 -3
- anemoi/datasets/commands/check.py +1 -1
- anemoi/datasets/commands/copy.py +1 -2
- anemoi/datasets/commands/create.py +1 -1
- anemoi/datasets/commands/inspect.py +27 -35
- anemoi/datasets/commands/recipe/__init__.py +93 -0
- anemoi/datasets/commands/recipe/format.py +55 -0
- anemoi/datasets/commands/recipe/migrate.py +555 -0
- anemoi/datasets/commands/validate.py +59 -0
- anemoi/datasets/compute/recentre.py +3 -6
- anemoi/datasets/create/__init__.py +64 -26
- anemoi/datasets/create/check.py +10 -12
- anemoi/datasets/create/chunks.py +1 -2
- anemoi/datasets/create/config.py +5 -6
- anemoi/datasets/create/input/__init__.py +44 -65
- anemoi/datasets/create/input/action.py +296 -238
- anemoi/datasets/create/input/context/__init__.py +71 -0
- anemoi/datasets/create/input/context/field.py +54 -0
- anemoi/datasets/create/input/data_sources.py +7 -9
- anemoi/datasets/create/input/misc.py +2 -75
- anemoi/datasets/create/input/repeated_dates.py +11 -130
- anemoi/datasets/{utils → create/input/result}/__init__.py +10 -1
- anemoi/datasets/create/input/{result.py → result/field.py} +36 -120
- anemoi/datasets/create/input/trace.py +1 -1
- anemoi/datasets/create/patch.py +1 -2
- anemoi/datasets/create/persistent.py +3 -5
- anemoi/datasets/create/size.py +1 -3
- anemoi/datasets/create/sources/accumulations.py +120 -145
- anemoi/datasets/create/sources/accumulations2.py +20 -53
- anemoi/datasets/create/sources/anemoi_dataset.py +46 -42
- anemoi/datasets/create/sources/constants.py +39 -40
- anemoi/datasets/create/sources/empty.py +22 -19
- anemoi/datasets/create/sources/fdb.py +133 -0
- anemoi/datasets/create/sources/forcings.py +29 -29
- anemoi/datasets/create/sources/grib.py +94 -78
- anemoi/datasets/create/sources/grib_index.py +57 -55
- anemoi/datasets/create/sources/hindcasts.py +57 -59
- anemoi/datasets/create/sources/legacy.py +10 -62
- anemoi/datasets/create/sources/mars.py +121 -149
- anemoi/datasets/create/sources/netcdf.py +28 -25
- anemoi/datasets/create/sources/opendap.py +28 -26
- anemoi/datasets/create/sources/patterns.py +4 -6
- anemoi/datasets/create/sources/recentre.py +46 -48
- anemoi/datasets/create/sources/repeated_dates.py +44 -0
- anemoi/datasets/create/sources/source.py +26 -51
- anemoi/datasets/create/sources/tendencies.py +68 -98
- anemoi/datasets/create/sources/xarray.py +4 -6
- anemoi/datasets/create/sources/xarray_support/__init__.py +40 -36
- anemoi/datasets/create/sources/xarray_support/coordinates.py +8 -12
- anemoi/datasets/create/sources/xarray_support/field.py +20 -16
- anemoi/datasets/create/sources/xarray_support/fieldlist.py +11 -15
- anemoi/datasets/create/sources/xarray_support/flavour.py +42 -42
- anemoi/datasets/create/sources/xarray_support/grid.py +15 -9
- anemoi/datasets/create/sources/xarray_support/metadata.py +19 -128
- anemoi/datasets/create/sources/xarray_support/patch.py +4 -6
- anemoi/datasets/create/sources/xarray_support/time.py +10 -13
- anemoi/datasets/create/sources/xarray_support/variable.py +21 -21
- anemoi/datasets/create/sources/xarray_zarr.py +28 -25
- anemoi/datasets/create/sources/zenodo.py +43 -41
- anemoi/datasets/create/statistics/__init__.py +3 -6
- anemoi/datasets/create/testing.py +4 -0
- anemoi/datasets/create/typing.py +1 -2
- anemoi/datasets/create/utils.py +0 -43
- anemoi/datasets/create/zarr.py +7 -2
- anemoi/datasets/data/__init__.py +15 -6
- anemoi/datasets/data/complement.py +7 -12
- anemoi/datasets/data/concat.py +5 -8
- anemoi/datasets/data/dataset.py +48 -47
- anemoi/datasets/data/debug.py +7 -9
- anemoi/datasets/data/ensemble.py +4 -6
- anemoi/datasets/data/fill_missing.py +7 -10
- anemoi/datasets/data/forwards.py +22 -26
- anemoi/datasets/data/grids.py +12 -168
- anemoi/datasets/data/indexing.py +9 -12
- anemoi/datasets/data/interpolate.py +7 -15
- anemoi/datasets/data/join.py +8 -12
- anemoi/datasets/data/masked.py +6 -11
- anemoi/datasets/data/merge.py +5 -9
- anemoi/datasets/data/misc.py +41 -45
- anemoi/datasets/data/missing.py +11 -16
- anemoi/datasets/data/observations/__init__.py +8 -14
- anemoi/datasets/data/padded.py +3 -5
- anemoi/datasets/data/records/backends/__init__.py +2 -2
- anemoi/datasets/data/rescale.py +5 -12
- anemoi/datasets/data/rolling_average.py +141 -0
- anemoi/datasets/data/select.py +13 -16
- anemoi/datasets/data/statistics.py +4 -7
- anemoi/datasets/data/stores.py +22 -29
- anemoi/datasets/data/subset.py +8 -11
- anemoi/datasets/data/unchecked.py +7 -11
- anemoi/datasets/data/xy.py +25 -21
- anemoi/datasets/dates/__init__.py +15 -18
- anemoi/datasets/dates/groups.py +7 -10
- anemoi/datasets/dumper.py +76 -0
- anemoi/datasets/grids.py +4 -185
- anemoi/datasets/schemas/recipe.json +131 -0
- anemoi/datasets/testing.py +93 -7
- anemoi/datasets/validate.py +598 -0
- {anemoi_datasets-0.5.26.dist-info → anemoi_datasets-0.5.28.dist-info}/METADATA +7 -4
- anemoi_datasets-0.5.28.dist-info/RECORD +134 -0
- anemoi/datasets/create/filter.py +0 -48
- anemoi/datasets/create/input/concat.py +0 -164
- anemoi/datasets/create/input/context.py +0 -89
- anemoi/datasets/create/input/empty.py +0 -54
- anemoi/datasets/create/input/filter.py +0 -118
- anemoi/datasets/create/input/function.py +0 -233
- anemoi/datasets/create/input/join.py +0 -130
- anemoi/datasets/create/input/pipe.py +0 -66
- anemoi/datasets/create/input/step.py +0 -177
- anemoi/datasets/create/input/template.py +0 -162
- anemoi_datasets-0.5.26.dist-info/RECORD +0 -131
- {anemoi_datasets-0.5.26.dist-info → anemoi_datasets-0.5.28.dist-info}/WHEEL +0 -0
- {anemoi_datasets-0.5.26.dist-info → anemoi_datasets-0.5.28.dist-info}/entry_points.txt +0 -0
- {anemoi_datasets-0.5.26.dist-info → anemoi_datasets-0.5.28.dist-info}/licenses/LICENSE +0 -0
- {anemoi_datasets-0.5.26.dist-info → anemoi_datasets-0.5.28.dist-info}/top_level.txt +0 -0
anemoi/datasets/__init__.py
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
# granted to it by virtue of its status as an intergovernmental organisation
|
|
8
8
|
# nor does it submit to any jurisdiction.
|
|
9
9
|
|
|
10
|
-
from typing import List
|
|
11
10
|
|
|
12
11
|
from .data import MissingDateError
|
|
13
12
|
from .data import add_dataset_path
|
|
@@ -23,7 +22,7 @@ except ImportError: # pragma: no cover
|
|
|
23
22
|
# Local copy or not installed with setuptools
|
|
24
23
|
__version__ = "999"
|
|
25
24
|
|
|
26
|
-
__all__:
|
|
25
|
+
__all__: list[str] = [
|
|
27
26
|
"add_dataset_path",
|
|
28
27
|
"add_named_dataset",
|
|
29
28
|
"list_dataset_names",
|
anemoi/datasets/_version.py
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
3
|
|
|
4
|
-
__all__ = [
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
5
12
|
|
|
6
13
|
TYPE_CHECKING = False
|
|
7
14
|
if TYPE_CHECKING:
|
|
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
|
|
|
9
16
|
from typing import Union
|
|
10
17
|
|
|
11
18
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
12
20
|
else:
|
|
13
21
|
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
14
23
|
|
|
15
24
|
version: str
|
|
16
25
|
__version__: str
|
|
17
26
|
__version_tuple__: VERSION_TUPLE
|
|
18
27
|
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
19
30
|
|
|
20
|
-
__version__ = version = '0.5.
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 5,
|
|
31
|
+
__version__ = version = '0.5.28'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 5, 28)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
@@ -77,7 +77,7 @@ class Check(Command):
|
|
|
77
77
|
|
|
78
78
|
recipe_filename = os.path.basename(recipe)
|
|
79
79
|
recipe_name = os.path.splitext(recipe_filename)[0]
|
|
80
|
-
in_recipe_name = yaml.safe_load(open(recipe,
|
|
80
|
+
in_recipe_name = yaml.safe_load(open(recipe, encoding="utf-8"))["name"]
|
|
81
81
|
if recipe_name != in_recipe_name:
|
|
82
82
|
print(f"Recipe name {recipe_name} does not match the name in the recipe file {in_recipe_name}")
|
|
83
83
|
|
anemoi/datasets/commands/copy.py
CHANGED
|
@@ -14,7 +14,6 @@ import sys
|
|
|
14
14
|
from concurrent.futures import ThreadPoolExecutor
|
|
15
15
|
from concurrent.futures import as_completed
|
|
16
16
|
from typing import Any
|
|
17
|
-
from typing import Optional
|
|
18
17
|
|
|
19
18
|
import tqdm
|
|
20
19
|
from anemoi.utils.remote import Transfer
|
|
@@ -136,7 +135,7 @@ class ZarrCopier:
|
|
|
136
135
|
return zarr.storage.NestedDirectoryStore(path)
|
|
137
136
|
return path
|
|
138
137
|
|
|
139
|
-
def copy_chunk(self, n: int, m: int, source: Any, target: Any, _copy: Any, verbosity: int) ->
|
|
138
|
+
def copy_chunk(self, n: int, m: int, source: Any, target: Any, _copy: Any, verbosity: int) -> slice | None:
|
|
140
139
|
"""Copy a chunk of data from source to target.
|
|
141
140
|
|
|
142
141
|
Parameters
|
|
@@ -14,10 +14,6 @@ import os
|
|
|
14
14
|
from copy import deepcopy
|
|
15
15
|
from functools import cached_property
|
|
16
16
|
from typing import Any
|
|
17
|
-
from typing import Dict
|
|
18
|
-
from typing import List
|
|
19
|
-
from typing import Optional
|
|
20
|
-
from typing import Union
|
|
21
17
|
|
|
22
18
|
import numpy as np
|
|
23
19
|
import semantic_version
|
|
@@ -39,7 +35,7 @@ from . import Command
|
|
|
39
35
|
LOG = logging.getLogger(__name__)
|
|
40
36
|
|
|
41
37
|
|
|
42
|
-
def compute_directory_size(path: str) ->
|
|
38
|
+
def compute_directory_size(path: str) -> tuple[int, int] | tuple[None, None]:
|
|
43
39
|
"""Compute the total size and number of files in a directory.
|
|
44
40
|
|
|
45
41
|
Parameters
|
|
@@ -104,7 +100,7 @@ def cos_local_time_bug(lon: float, date: datetime.datetime) -> float:
|
|
|
104
100
|
return np.cos(radians)
|
|
105
101
|
|
|
106
102
|
|
|
107
|
-
def find(config:
|
|
103
|
+
def find(config: dict | list, name: str) -> Any:
|
|
108
104
|
"""Recursively search for a key in a nested dictionary or list.
|
|
109
105
|
|
|
110
106
|
Parameters
|
|
@@ -167,7 +163,7 @@ class Version:
|
|
|
167
163
|
print(f"🔢 Format version: {self.version}")
|
|
168
164
|
|
|
169
165
|
@property
|
|
170
|
-
def name_to_index(self) ->
|
|
166
|
+
def name_to_index(self) -> dict[str, int]:
|
|
171
167
|
"""Get a mapping of variable names to their indices."""
|
|
172
168
|
return find(self.metadata, "name_to_index")
|
|
173
169
|
|
|
@@ -208,30 +204,30 @@ class Version:
|
|
|
208
204
|
return self.metadata["resolution"]
|
|
209
205
|
|
|
210
206
|
@property
|
|
211
|
-
def field_shape(self) ->
|
|
207
|
+
def field_shape(self) -> tuple | None:
|
|
212
208
|
"""Get the field shape of the dataset."""
|
|
213
209
|
return self.metadata.get("field_shape")
|
|
214
210
|
|
|
215
211
|
@property
|
|
216
|
-
def proj_string(self) ->
|
|
212
|
+
def proj_string(self) -> str | None:
|
|
217
213
|
"""Get the projection string of the dataset."""
|
|
218
214
|
return self.metadata.get("proj_string")
|
|
219
215
|
|
|
220
216
|
@property
|
|
221
|
-
def shape(self) ->
|
|
217
|
+
def shape(self) -> tuple | None:
|
|
222
218
|
"""Get the shape of the dataset."""
|
|
223
219
|
if self.data and hasattr(self.data, "shape"):
|
|
224
220
|
return self.data.shape
|
|
225
221
|
|
|
226
222
|
@property
|
|
227
|
-
def n_missing_dates(self) ->
|
|
223
|
+
def n_missing_dates(self) -> int | None:
|
|
228
224
|
"""Get the number of missing dates in the dataset."""
|
|
229
225
|
if "missing_dates" in self.metadata:
|
|
230
226
|
return len(self.metadata["missing_dates"])
|
|
231
227
|
return None
|
|
232
228
|
|
|
233
229
|
@property
|
|
234
|
-
def uncompressed_data_size(self) ->
|
|
230
|
+
def uncompressed_data_size(self) -> int | None:
|
|
235
231
|
"""Get the uncompressed data size of the dataset."""
|
|
236
232
|
if self.data and hasattr(self.data, "dtype") and hasattr(self.data, "size"):
|
|
237
233
|
return self.data.dtype.itemsize * self.data.size
|
|
@@ -258,7 +254,7 @@ class Version:
|
|
|
258
254
|
print()
|
|
259
255
|
shape_str = "📐 Shape : "
|
|
260
256
|
if self.shape:
|
|
261
|
-
shape_str += " × ".join(["{:,}"
|
|
257
|
+
shape_str += " × ".join([f"{s:,}" for s in self.shape])
|
|
262
258
|
if self.uncompressed_data_size:
|
|
263
259
|
shape_str += f" ({bytes(self.uncompressed_data_size)})"
|
|
264
260
|
print(shape_str)
|
|
@@ -293,17 +289,17 @@ class Version:
|
|
|
293
289
|
print()
|
|
294
290
|
|
|
295
291
|
@property
|
|
296
|
-
def variables(self) ->
|
|
292
|
+
def variables(self) -> list[str]:
|
|
297
293
|
"""Get the list of variables in the dataset."""
|
|
298
294
|
return [v[0] for v in sorted(self.name_to_index.items(), key=lambda x: x[1])]
|
|
299
295
|
|
|
300
296
|
@property
|
|
301
|
-
def total_size(self) ->
|
|
297
|
+
def total_size(self) -> int | None:
|
|
302
298
|
"""Get the total size of the dataset."""
|
|
303
299
|
return self.zarr.attrs.get("total_size")
|
|
304
300
|
|
|
305
301
|
@property
|
|
306
|
-
def total_number_of_files(self) ->
|
|
302
|
+
def total_number_of_files(self) -> int | None:
|
|
307
303
|
"""Get the total number of files in the dataset."""
|
|
308
304
|
return self.zarr.attrs.get("total_number_of_files")
|
|
309
305
|
|
|
@@ -348,7 +344,7 @@ class Version:
|
|
|
348
344
|
return False
|
|
349
345
|
|
|
350
346
|
@property
|
|
351
|
-
def statistics_started(self) ->
|
|
347
|
+
def statistics_started(self) -> datetime.datetime | None:
|
|
352
348
|
"""Get the timestamp when statistics computation started."""
|
|
353
349
|
for d in reversed(self.metadata.get("history", [])):
|
|
354
350
|
if d["action"] == "compute_statistics_start":
|
|
@@ -356,12 +352,12 @@ class Version:
|
|
|
356
352
|
return None
|
|
357
353
|
|
|
358
354
|
@property
|
|
359
|
-
def build_flags(self) ->
|
|
355
|
+
def build_flags(self) -> NDArray[Any] | None:
|
|
360
356
|
"""Get the build flags of the dataset."""
|
|
361
357
|
return self.zarr.get("_build_flags")
|
|
362
358
|
|
|
363
359
|
@cached_property
|
|
364
|
-
def copy_flags(self) ->
|
|
360
|
+
def copy_flags(self) -> NDArray[Any] | None:
|
|
365
361
|
"""Get the copy flags of the dataset."""
|
|
366
362
|
if "_copy" not in self.zarr:
|
|
367
363
|
return None
|
|
@@ -381,7 +377,7 @@ class Version:
|
|
|
381
377
|
return not all(self.copy_flags)
|
|
382
378
|
|
|
383
379
|
@property
|
|
384
|
-
def build_lengths(self) ->
|
|
380
|
+
def build_lengths(self) -> NDArray | None:
|
|
385
381
|
"""Get the build lengths of the dataset."""
|
|
386
382
|
return self.zarr.get("_build_lengths")
|
|
387
383
|
|
|
@@ -396,17 +392,13 @@ class Version:
|
|
|
396
392
|
print(
|
|
397
393
|
"📈 Progress:",
|
|
398
394
|
progress(built, total, width=50),
|
|
399
|
-
"{
|
|
395
|
+
f"{built / total * 100:.0f}%",
|
|
400
396
|
)
|
|
401
397
|
return
|
|
402
398
|
|
|
403
|
-
|
|
404
|
-
print("🪫 Dataset not initialised")
|
|
405
|
-
return
|
|
406
|
-
|
|
407
|
-
build_flags = self.build_flags
|
|
399
|
+
build_flags = self.build_flags or np.array([], dtype=bool)
|
|
408
400
|
|
|
409
|
-
build_lengths = self.build_lengths
|
|
401
|
+
build_lengths = self.build_lengths or np.array([], dtype=bool)
|
|
410
402
|
assert build_flags.size == build_lengths.size
|
|
411
403
|
|
|
412
404
|
latest_write_timestamp = self.zarr.attrs.get("latest_write_timestamp")
|
|
@@ -422,7 +414,7 @@ class Version:
|
|
|
422
414
|
print(
|
|
423
415
|
"📈 Progress:",
|
|
424
416
|
progress(built, total, width=50),
|
|
425
|
-
"{
|
|
417
|
+
f"{built / total * 100:.0f}%",
|
|
426
418
|
)
|
|
427
419
|
start = self.initialised
|
|
428
420
|
if self.initialised:
|
|
@@ -623,7 +615,7 @@ class Version0_6(Version):
|
|
|
623
615
|
"""Represents version 0.6 of a dataset."""
|
|
624
616
|
|
|
625
617
|
@property
|
|
626
|
-
def initialised(self) ->
|
|
618
|
+
def initialised(self) -> datetime.datetime | None:
|
|
627
619
|
"""Get the initialization timestamp of the dataset."""
|
|
628
620
|
for record in self.metadata.get("history", []):
|
|
629
621
|
if record["action"] == "initialised":
|
|
@@ -659,12 +651,12 @@ class Version0_6(Version):
|
|
|
659
651
|
return all(build_flags)
|
|
660
652
|
|
|
661
653
|
@property
|
|
662
|
-
def name_to_index(self) ->
|
|
654
|
+
def name_to_index(self) -> dict[str, int]:
|
|
663
655
|
"""Get a mapping of variable names to their indices."""
|
|
664
656
|
return {n: i for i, n in enumerate(self.metadata["variables"])}
|
|
665
657
|
|
|
666
658
|
@property
|
|
667
|
-
def variables(self) ->
|
|
659
|
+
def variables(self) -> list[str]:
|
|
668
660
|
"""Get the list of variables in the dataset."""
|
|
669
661
|
return self.metadata["variables"]
|
|
670
662
|
|
|
@@ -706,7 +698,7 @@ class Version0_13(Version0_12):
|
|
|
706
698
|
"""Represents version 0.13 of a dataset."""
|
|
707
699
|
|
|
708
700
|
@property
|
|
709
|
-
def build_flags(self) ->
|
|
701
|
+
def build_flags(self) -> NDArray | None:
|
|
710
702
|
"""Get the build flags for the dataset."""
|
|
711
703
|
if "_build" not in self.zarr:
|
|
712
704
|
return None
|
|
@@ -714,7 +706,7 @@ class Version0_13(Version0_12):
|
|
|
714
706
|
return build.get("flags")
|
|
715
707
|
|
|
716
708
|
@property
|
|
717
|
-
def build_lengths(self) ->
|
|
709
|
+
def build_lengths(self) -> NDArray | None:
|
|
718
710
|
"""Get the build lengths for the dataset."""
|
|
719
711
|
if "_build" not in self.zarr:
|
|
720
712
|
return None
|
|
@@ -792,10 +784,10 @@ class InspectZarr(Command):
|
|
|
792
784
|
|
|
793
785
|
try:
|
|
794
786
|
if progress:
|
|
795
|
-
|
|
787
|
+
version.progress()
|
|
796
788
|
|
|
797
789
|
if statistics:
|
|
798
|
-
|
|
790
|
+
version.brute_force_statistics()
|
|
799
791
|
|
|
800
792
|
version.info(detailed, size)
|
|
801
793
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# (C) Copyright 2024 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
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import logging
|
|
13
|
+
import sys
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import yaml
|
|
17
|
+
|
|
18
|
+
from anemoi.datasets.create import validate_config
|
|
19
|
+
|
|
20
|
+
from .. import Command
|
|
21
|
+
from .format import format_recipe
|
|
22
|
+
from .migrate import migrate_recipe
|
|
23
|
+
|
|
24
|
+
LOG = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Recipe(Command):
|
|
28
|
+
def add_arguments(self, command_parser: Any) -> None:
|
|
29
|
+
"""Add arguments to the command parser.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
command_parser : Any
|
|
34
|
+
Command parser object.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
command_parser.add_argument("--validate", action="store_true", help="Validate recipe.")
|
|
38
|
+
command_parser.add_argument("--format", action="store_true", help="Format the recipe.")
|
|
39
|
+
command_parser.add_argument("--migrate", action="store_true", help="Migrate the recipe to the latest version.")
|
|
40
|
+
|
|
41
|
+
group = command_parser.add_mutually_exclusive_group()
|
|
42
|
+
group.add_argument("--inplace", action="store_true", help="Overwrite the recipe file in place.")
|
|
43
|
+
group.add_argument("--output", type=str, help="Output file path for the converted recipe.")
|
|
44
|
+
|
|
45
|
+
command_parser.add_argument(
|
|
46
|
+
"path",
|
|
47
|
+
help="Path to recipe.",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def run(self, args: Any) -> None:
|
|
51
|
+
|
|
52
|
+
if not args.validate and not args.format and not args.migrate:
|
|
53
|
+
args.validate = True
|
|
54
|
+
|
|
55
|
+
with open(args.path) as file:
|
|
56
|
+
config = yaml.safe_load(file)
|
|
57
|
+
|
|
58
|
+
assert isinstance(config, dict)
|
|
59
|
+
|
|
60
|
+
if args.validate:
|
|
61
|
+
if args.inplace and (not args.format and not args.migrate):
|
|
62
|
+
argparse.ArgumentError(None, "--inplace is not supported with --validate.")
|
|
63
|
+
|
|
64
|
+
if args.output and (not args.format and not args.migrate):
|
|
65
|
+
argparse.ArgumentError(None, "--output is not supported with --validate.")
|
|
66
|
+
|
|
67
|
+
validate_config(config)
|
|
68
|
+
LOG.info(f"{args.path}: Recipe is valid.")
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
if args.migrate:
|
|
72
|
+
config = migrate_recipe(args, config)
|
|
73
|
+
if config is None:
|
|
74
|
+
LOG.info(f"{args.path}: No changes needed.")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
args.format = True
|
|
78
|
+
|
|
79
|
+
if args.format:
|
|
80
|
+
formatted = format_recipe(args, config)
|
|
81
|
+
assert "dates" in formatted
|
|
82
|
+
f = sys.stdout
|
|
83
|
+
if args.output:
|
|
84
|
+
f = open(args.output, "w")
|
|
85
|
+
|
|
86
|
+
if args.inplace:
|
|
87
|
+
f = open(args.path, "w")
|
|
88
|
+
|
|
89
|
+
print(formatted, file=f)
|
|
90
|
+
f.close()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
command = Recipe
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
|
|
11
|
+
import datetime
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
from ...dumper import yaml_dump
|
|
15
|
+
|
|
16
|
+
LOG = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def make_dates(config):
|
|
20
|
+
if isinstance(config, dict):
|
|
21
|
+
return {k: make_dates(v) for k, v in config.items()}
|
|
22
|
+
if isinstance(config, list):
|
|
23
|
+
return [make_dates(v) for v in config]
|
|
24
|
+
if isinstance(config, str):
|
|
25
|
+
try:
|
|
26
|
+
return datetime.datetime.fromisoformat(config)
|
|
27
|
+
except ValueError:
|
|
28
|
+
return config
|
|
29
|
+
return config
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
ORDER = (
|
|
33
|
+
"name",
|
|
34
|
+
"description",
|
|
35
|
+
"dataset_status",
|
|
36
|
+
"licence",
|
|
37
|
+
"attribution",
|
|
38
|
+
"env",
|
|
39
|
+
"dates",
|
|
40
|
+
"common",
|
|
41
|
+
"data_sources",
|
|
42
|
+
"input",
|
|
43
|
+
"output",
|
|
44
|
+
"statistics",
|
|
45
|
+
"build",
|
|
46
|
+
"platform",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def format_recipe(args, config: dict) -> str:
|
|
51
|
+
|
|
52
|
+
config = make_dates(config)
|
|
53
|
+
assert config
|
|
54
|
+
|
|
55
|
+
return yaml_dump(config, order=ORDER)
|