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.
Files changed (116) hide show
  1. anemoi/datasets/__init__.py +1 -2
  2. anemoi/datasets/_version.py +16 -3
  3. anemoi/datasets/commands/check.py +1 -1
  4. anemoi/datasets/commands/copy.py +1 -2
  5. anemoi/datasets/commands/create.py +1 -1
  6. anemoi/datasets/commands/inspect.py +27 -35
  7. anemoi/datasets/commands/recipe/__init__.py +93 -0
  8. anemoi/datasets/commands/recipe/format.py +55 -0
  9. anemoi/datasets/commands/recipe/migrate.py +555 -0
  10. anemoi/datasets/commands/validate.py +59 -0
  11. anemoi/datasets/compute/recentre.py +3 -6
  12. anemoi/datasets/create/__init__.py +64 -26
  13. anemoi/datasets/create/check.py +10 -12
  14. anemoi/datasets/create/chunks.py +1 -2
  15. anemoi/datasets/create/config.py +5 -6
  16. anemoi/datasets/create/input/__init__.py +44 -65
  17. anemoi/datasets/create/input/action.py +296 -238
  18. anemoi/datasets/create/input/context/__init__.py +71 -0
  19. anemoi/datasets/create/input/context/field.py +54 -0
  20. anemoi/datasets/create/input/data_sources.py +7 -9
  21. anemoi/datasets/create/input/misc.py +2 -75
  22. anemoi/datasets/create/input/repeated_dates.py +11 -130
  23. anemoi/datasets/{utils → create/input/result}/__init__.py +10 -1
  24. anemoi/datasets/create/input/{result.py → result/field.py} +36 -120
  25. anemoi/datasets/create/input/trace.py +1 -1
  26. anemoi/datasets/create/patch.py +1 -2
  27. anemoi/datasets/create/persistent.py +3 -5
  28. anemoi/datasets/create/size.py +1 -3
  29. anemoi/datasets/create/sources/accumulations.py +120 -145
  30. anemoi/datasets/create/sources/accumulations2.py +20 -53
  31. anemoi/datasets/create/sources/anemoi_dataset.py +46 -42
  32. anemoi/datasets/create/sources/constants.py +39 -40
  33. anemoi/datasets/create/sources/empty.py +22 -19
  34. anemoi/datasets/create/sources/fdb.py +133 -0
  35. anemoi/datasets/create/sources/forcings.py +29 -29
  36. anemoi/datasets/create/sources/grib.py +94 -78
  37. anemoi/datasets/create/sources/grib_index.py +57 -55
  38. anemoi/datasets/create/sources/hindcasts.py +57 -59
  39. anemoi/datasets/create/sources/legacy.py +10 -62
  40. anemoi/datasets/create/sources/mars.py +121 -149
  41. anemoi/datasets/create/sources/netcdf.py +28 -25
  42. anemoi/datasets/create/sources/opendap.py +28 -26
  43. anemoi/datasets/create/sources/patterns.py +4 -6
  44. anemoi/datasets/create/sources/recentre.py +46 -48
  45. anemoi/datasets/create/sources/repeated_dates.py +44 -0
  46. anemoi/datasets/create/sources/source.py +26 -51
  47. anemoi/datasets/create/sources/tendencies.py +68 -98
  48. anemoi/datasets/create/sources/xarray.py +4 -6
  49. anemoi/datasets/create/sources/xarray_support/__init__.py +40 -36
  50. anemoi/datasets/create/sources/xarray_support/coordinates.py +8 -12
  51. anemoi/datasets/create/sources/xarray_support/field.py +20 -16
  52. anemoi/datasets/create/sources/xarray_support/fieldlist.py +11 -15
  53. anemoi/datasets/create/sources/xarray_support/flavour.py +42 -42
  54. anemoi/datasets/create/sources/xarray_support/grid.py +15 -9
  55. anemoi/datasets/create/sources/xarray_support/metadata.py +19 -128
  56. anemoi/datasets/create/sources/xarray_support/patch.py +4 -6
  57. anemoi/datasets/create/sources/xarray_support/time.py +10 -13
  58. anemoi/datasets/create/sources/xarray_support/variable.py +21 -21
  59. anemoi/datasets/create/sources/xarray_zarr.py +28 -25
  60. anemoi/datasets/create/sources/zenodo.py +43 -41
  61. anemoi/datasets/create/statistics/__init__.py +3 -6
  62. anemoi/datasets/create/testing.py +4 -0
  63. anemoi/datasets/create/typing.py +1 -2
  64. anemoi/datasets/create/utils.py +0 -43
  65. anemoi/datasets/create/zarr.py +7 -2
  66. anemoi/datasets/data/__init__.py +15 -6
  67. anemoi/datasets/data/complement.py +7 -12
  68. anemoi/datasets/data/concat.py +5 -8
  69. anemoi/datasets/data/dataset.py +48 -47
  70. anemoi/datasets/data/debug.py +7 -9
  71. anemoi/datasets/data/ensemble.py +4 -6
  72. anemoi/datasets/data/fill_missing.py +7 -10
  73. anemoi/datasets/data/forwards.py +22 -26
  74. anemoi/datasets/data/grids.py +12 -168
  75. anemoi/datasets/data/indexing.py +9 -12
  76. anemoi/datasets/data/interpolate.py +7 -15
  77. anemoi/datasets/data/join.py +8 -12
  78. anemoi/datasets/data/masked.py +6 -11
  79. anemoi/datasets/data/merge.py +5 -9
  80. anemoi/datasets/data/misc.py +41 -45
  81. anemoi/datasets/data/missing.py +11 -16
  82. anemoi/datasets/data/observations/__init__.py +8 -14
  83. anemoi/datasets/data/padded.py +3 -5
  84. anemoi/datasets/data/records/backends/__init__.py +2 -2
  85. anemoi/datasets/data/rescale.py +5 -12
  86. anemoi/datasets/data/rolling_average.py +141 -0
  87. anemoi/datasets/data/select.py +13 -16
  88. anemoi/datasets/data/statistics.py +4 -7
  89. anemoi/datasets/data/stores.py +22 -29
  90. anemoi/datasets/data/subset.py +8 -11
  91. anemoi/datasets/data/unchecked.py +7 -11
  92. anemoi/datasets/data/xy.py +25 -21
  93. anemoi/datasets/dates/__init__.py +15 -18
  94. anemoi/datasets/dates/groups.py +7 -10
  95. anemoi/datasets/dumper.py +76 -0
  96. anemoi/datasets/grids.py +4 -185
  97. anemoi/datasets/schemas/recipe.json +131 -0
  98. anemoi/datasets/testing.py +93 -7
  99. anemoi/datasets/validate.py +598 -0
  100. {anemoi_datasets-0.5.26.dist-info → anemoi_datasets-0.5.28.dist-info}/METADATA +7 -4
  101. anemoi_datasets-0.5.28.dist-info/RECORD +134 -0
  102. anemoi/datasets/create/filter.py +0 -48
  103. anemoi/datasets/create/input/concat.py +0 -164
  104. anemoi/datasets/create/input/context.py +0 -89
  105. anemoi/datasets/create/input/empty.py +0 -54
  106. anemoi/datasets/create/input/filter.py +0 -118
  107. anemoi/datasets/create/input/function.py +0 -233
  108. anemoi/datasets/create/input/join.py +0 -130
  109. anemoi/datasets/create/input/pipe.py +0 -66
  110. anemoi/datasets/create/input/step.py +0 -177
  111. anemoi/datasets/create/input/template.py +0 -162
  112. anemoi_datasets-0.5.26.dist-info/RECORD +0 -131
  113. {anemoi_datasets-0.5.26.dist-info → anemoi_datasets-0.5.28.dist-info}/WHEEL +0 -0
  114. {anemoi_datasets-0.5.26.dist-info → anemoi_datasets-0.5.28.dist-info}/entry_points.txt +0 -0
  115. {anemoi_datasets-0.5.26.dist-info → anemoi_datasets-0.5.28.dist-info}/licenses/LICENSE +0 -0
  116. {anemoi_datasets-0.5.26.dist-info → anemoi_datasets-0.5.28.dist-info}/top_level.txt +0 -0
@@ -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__: List[str] = [
25
+ __all__: list[str] = [
27
26
  "add_dataset_path",
28
27
  "add_named_dataset",
29
28
  "list_dataset_names",
@@ -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__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
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.26'
21
- __version_tuple__ = version_tuple = (0, 5, 26)
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, "r", encoding="utf-8"))["name"]
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
 
@@ -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) -> Optional[slice]:
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
@@ -120,7 +120,7 @@ class Create(Command):
120
120
  task("finalise", options)
121
121
 
122
122
  task("init_additions", options)
123
- task("run_additions", options)
123
+ task("load_additions", options)
124
124
  task("finalise_additions", options)
125
125
 
126
126
  task("patch", options)
@@ -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) -> Union[tuple[int, int], tuple[None, None]]:
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: Union[dict, list], name: str) -> Any:
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) -> Dict[str, int]:
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) -> Optional[tuple]:
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) -> Optional[str]:
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) -> Optional[tuple]:
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) -> Optional[int]:
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) -> Optional[int]:
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(["{:,}".format(s) for s in self.shape])
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) -> List[str]:
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) -> Optional[int]:
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) -> Optional[int]:
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) -> Optional[datetime.datetime]:
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) -> Optional[NDArray[Any]]:
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) -> Optional[NDArray[Any]]:
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) -> Optional[NDArray]:
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
- "{:.0f}%".format(built / total * 100),
395
+ f"{built / total * 100:.0f}%",
400
396
  )
401
397
  return
402
398
 
403
- if self.build_flags is None:
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
- "{:.0f}%".format(built / total * 100),
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) -> Optional[datetime.datetime]:
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) -> Dict[str, int]:
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) -> List[str]:
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) -> Optional[NDArray]:
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) -> Optional[NDArray]:
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
- return version.progress()
787
+ version.progress()
796
788
 
797
789
  if statistics:
798
- return version.brute_force_statistics()
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)