anemoi-utils 0.4.29__tar.gz → 0.4.31__tar.gz

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 (108) hide show
  1. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.github/workflows/python-pull-request.yml +1 -1
  2. anemoi_utils-0.4.31/.release-please-manifest.json +3 -0
  3. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/CHANGELOG.md +14 -0
  4. {anemoi_utils-0.4.29/src/anemoi_utils.egg-info → anemoi_utils-0.4.31}/PKG-INFO +2 -3
  5. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/docs/conf.py +2 -2
  6. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/pyproject.toml +1 -2
  7. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/_version.py +16 -3
  8. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/caching.py +4 -5
  9. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/checkpoints.py +1 -1
  10. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/cli.py +2 -3
  11. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/commands/metadata.py +1 -2
  12. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/compatibility.py +2 -6
  13. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/config.py +44 -21
  14. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/dates.py +17 -24
  15. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/grib.py +4 -6
  16. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/grids.py +2 -5
  17. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/hindcasts.py +3 -5
  18. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/humanize.py +33 -40
  19. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/mars/__init__.py +4 -7
  20. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/mars/requests.py +1 -2
  21. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/provenance.py +13 -18
  22. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/registry.py +6 -11
  23. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/remote/__init__.py +2 -3
  24. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/remote/s3.py +1 -1
  25. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/rules.py +10 -14
  26. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/s3.py +2 -3
  27. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/text.py +6 -9
  28. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31/src/anemoi_utils.egg-info}/PKG-INFO +2 -3
  29. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/tests/test_utils.py +34 -0
  30. anemoi_utils-0.4.29/.release-please-manifest.json +0 -3
  31. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.gitattributes +0 -0
  32. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.github/CODEOWNERS +0 -0
  33. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.github/ci-hpc-config.yml +0 -0
  34. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.github/dependabot.yml +0 -0
  35. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.github/labeler.yml +0 -0
  36. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.github/pull_request_template.md +0 -0
  37. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.github/workflows/downstream-ci-hpc.yml +0 -0
  38. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.github/workflows/pr-conventional-commit.yml +0 -0
  39. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.github/workflows/pr-label-conventional-commits.yml +0 -0
  40. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.github/workflows/pr-label-file-based.yml +0 -0
  41. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.github/workflows/pr-label-public.yml +0 -0
  42. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.github/workflows/python-publish.yml +0 -0
  43. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.github/workflows/readthedocs-pr-update.yml +0 -0
  44. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.github/workflows/release-please.yml +0 -0
  45. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.gitignore +0 -0
  46. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.pre-commit-config.yaml +0 -0
  47. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.readthedocs.yaml +0 -0
  48. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/.release-please-config.json +0 -0
  49. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/CONTRIBUTORS.md +0 -0
  50. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/LICENSE +0 -0
  51. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/README.md +0 -0
  52. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/docs/Makefile +0 -0
  53. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/docs/_static/logo.png +0 -0
  54. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/docs/_static/style.css +0 -0
  55. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/docs/_templates/.gitkeep +0 -0
  56. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/docs/_templates/apidoc/package.rst.jinja +0 -0
  57. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/docs/index.rst +0 -0
  58. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/docs/installing.rst +0 -0
  59. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/docs/modules/checkpoints.rst +0 -0
  60. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/docs/modules/config.rst +0 -0
  61. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/docs/modules/dates.rst +0 -0
  62. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/docs/modules/grib.rst +0 -0
  63. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/docs/modules/humanize.rst +0 -0
  64. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/docs/modules/provenance.rst +0 -0
  65. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/docs/modules/s3.rst +0 -0
  66. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/docs/modules/testing.rst +0 -0
  67. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/docs/modules/text.rst +0 -0
  68. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/docs/scripts/api_build.sh +0 -0
  69. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/setup.cfg +0 -0
  70. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/__init__.py +0 -0
  71. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/__main__.py +0 -0
  72. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/commands/__init__.py +0 -0
  73. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/commands/config.py +0 -0
  74. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/commands/requests.py +0 -0
  75. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/commands/transfer.py +0 -0
  76. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/devtools.py +0 -0
  77. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/logs.py +0 -0
  78. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/mars/mars.yaml +0 -0
  79. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/mlflow/__init__.py +0 -0
  80. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/mlflow/auth.py +0 -0
  81. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/mlflow/client.py +0 -0
  82. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/mlflow/utils.py +0 -0
  83. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/remote/ssh.py +0 -0
  84. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/sanitise.py +0 -0
  85. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/sanitize.py +0 -0
  86. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/schemas/__init__.py +0 -0
  87. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/schemas/errors.py +0 -0
  88. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/testing.py +0 -0
  89. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi/utils/timer.py +0 -0
  90. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi_utils.egg-info/SOURCES.txt +0 -0
  91. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi_utils.egg-info/dependency_links.txt +0 -0
  92. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi_utils.egg-info/entry_points.txt +0 -0
  93. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi_utils.egg-info/requires.txt +0 -0
  94. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/src/anemoi_utils.egg-info/top_level.txt +0 -0
  95. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/tests/test-transfer-data/directory/b/c/x +0 -0
  96. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/tests/test-transfer-data/directory/b/y +0 -0
  97. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/tests/test-transfer-data/directory/exotic filename ;^/"'[=.,#]()/303/252/303/274/303/247/303/262/342/234/205.txt" +0 -0
  98. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/tests/test-transfer-data/directory/z +0 -0
  99. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/tests/test-transfer-data/file +0 -0
  100. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/tests/test_caching.py +0 -0
  101. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/tests/test_compatibility.py +0 -0
  102. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/tests/test_dates.py +0 -0
  103. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/tests/test_frequency.py +0 -0
  104. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/tests/test_mlflow_auth.py +0 -0
  105. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/tests/test_mlflow_client.py +0 -0
  106. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/tests/test_provenance.py +0 -0
  107. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/tests/test_remote.py +0 -0
  108. {anemoi_utils-0.4.29 → anemoi_utils-0.4.31}/tests/test_sanitise.py +0 -0
@@ -19,7 +19,7 @@ jobs:
19
19
  checks:
20
20
  strategy:
21
21
  matrix:
22
- python-version: ["3.9", "3.10", "3.11", "3.12"]
22
+ python-version: ["3.10", "3.11", "3.12"]
23
23
  uses: ecmwf/reusable-workflows/.github/workflows/qa-pytest-pyproject.yml@v2
24
24
  with:
25
25
  python-version: ${{ matrix.python-version }}
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.4.31"
3
+ }
@@ -8,6 +8,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  Please add your functional changes to the appropriate section in the PR.
9
9
  Keep it human-readable, your future self will thank you!
10
10
 
11
+ ## [0.4.31](https://github.com/ecmwf/anemoi-utils/compare/0.4.30...0.4.31) (2025-08-04)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * Remove too many warnings ([#193](https://github.com/ecmwf/anemoi-utils/issues/193)) ([df6862b](https://github.com/ecmwf/anemoi-utils/commit/df6862bf829e67651ccc97cbaac9f38096ad4d34))
17
+
18
+ ## [0.4.30](https://github.com/ecmwf/anemoi-utils/compare/0.4.29...0.4.30) (2025-07-31)
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * Refactor code for casting dotdicts and apply this in getitem and setitem methods ([#169](https://github.com/ecmwf/anemoi-utils/issues/169)) ([e91aecf](https://github.com/ecmwf/anemoi-utils/commit/e91aecf6699a0daaed6f79e92b4ebc57cd4abe36))
24
+
11
25
  ## [0.4.29](https://github.com/ecmwf/anemoi-utils/compare/0.4.28...0.4.29) (2025-07-22)
12
26
 
13
27
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anemoi-utils
3
- Version: 0.4.29
3
+ Version: 0.4.31
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
@@ -215,14 +215,13 @@ Classifier: Intended Audience :: Developers
215
215
  Classifier: License :: OSI Approved :: Apache Software License
216
216
  Classifier: Operating System :: OS Independent
217
217
  Classifier: Programming Language :: Python :: 3 :: Only
218
- Classifier: Programming Language :: Python :: 3.9
219
218
  Classifier: Programming Language :: Python :: 3.10
220
219
  Classifier: Programming Language :: Python :: 3.11
221
220
  Classifier: Programming Language :: Python :: 3.12
222
221
  Classifier: Programming Language :: Python :: 3.13
223
222
  Classifier: Programming Language :: Python :: Implementation :: CPython
224
223
  Classifier: Programming Language :: Python :: Implementation :: PyPy
225
- Requires-Python: >=3.9
224
+ Requires-Python: >=3.10
226
225
  License-File: LICENSE
227
226
  Requires-Dist: aniso8601
228
227
  Requires-Dist: deprecation
@@ -35,9 +35,9 @@ year = datetime.datetime.now().year
35
35
  if year == 2024:
36
36
  years = "2024"
37
37
  else:
38
- years = "2024-%s" % (year,)
38
+ years = f"2024-{year}"
39
39
 
40
- copyright = "%s, Anemoi contributors" % (years,)
40
+ copyright = f"{years}, Anemoi contributors"
41
41
 
42
42
  try:
43
43
  from anemoi.utils._version import __version__
@@ -21,7 +21,7 @@ authors = [
21
21
  { name = "European Centre for Medium-Range Weather Forecasts (ECMWF)", email = "software.support@ecmwf.int" },
22
22
  ]
23
23
 
24
- requires-python = ">=3.9"
24
+ requires-python = ">=3.10"
25
25
 
26
26
  classifiers = [
27
27
  "Development Status :: 4 - Beta",
@@ -29,7 +29,6 @@ classifiers = [
29
29
  "License :: OSI Approved :: Apache Software License",
30
30
  "Operating System :: OS Independent",
31
31
  "Programming Language :: Python :: 3 :: Only",
32
- "Programming Language :: Python :: 3.9",
33
32
  "Programming Language :: Python :: 3.10",
34
33
  "Programming Language :: Python :: 3.11",
35
34
  "Programming Language :: Python :: 3.12",
@@ -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.4.29'
21
- __version_tuple__ = version_tuple = (0, 4, 29)
31
+ __version__ = version = '0.4.31'
32
+ __version_tuple__ = version_tuple = (0, 4, 31)
33
+
34
+ __commit_id__ = commit_id = 'g2684cac7a'
@@ -12,10 +12,9 @@ import hashlib
12
12
  import json
13
13
  import os
14
14
  import time
15
+ from collections.abc import Callable
15
16
  from threading import Lock
16
17
  from typing import Any
17
- from typing import Callable
18
- from typing import Optional
19
18
 
20
19
  import numpy as np
21
20
 
@@ -61,7 +60,7 @@ class Cacher:
61
60
  Private class, do not use directly.
62
61
  """
63
62
 
64
- def __init__(self, collection: str, expires: Optional[int]):
63
+ def __init__(self, collection: str, expires: int | None):
65
64
  """Initialize the Cacher.
66
65
 
67
66
  Parameters
@@ -181,7 +180,7 @@ class JsonCacher(Cacher):
181
180
  dict
182
181
  The loaded data
183
182
  """
184
- with open(path, "r") as f:
183
+ with open(path) as f:
185
184
  return json.load(f)
186
185
 
187
186
 
@@ -226,7 +225,7 @@ class NpzCacher(Cacher):
226
225
 
227
226
 
228
227
  # This function is the main entry point for the caching mechanism for the other anemoi packages
229
- def cached(collection: str = "default", expires: Optional[int] = None, encoding: str = "json") -> Callable:
228
+ def cached(collection: str = "default", expires: int | None = None, encoding: str = "json") -> Callable:
230
229
  """Decorator to cache the result of a function.
231
230
 
232
231
  Default is to use a json file to store the cache, but you can also use npz files
@@ -17,8 +17,8 @@ import logging
17
17
  import os
18
18
  import time
19
19
  import zipfile
20
+ from collections.abc import Callable
20
21
  from tempfile import TemporaryDirectory
21
- from typing import Callable
22
22
 
23
23
  import tqdm
24
24
 
@@ -14,8 +14,7 @@ import logging
14
14
  import os
15
15
  import sys
16
16
  import traceback
17
- from typing import Callable
18
- from typing import Optional
17
+ from collections.abc import Callable
19
18
 
20
19
  try:
21
20
  import argcomplete
@@ -187,7 +186,7 @@ def register_commands(here: str, package: str, select: Callable, fail: Callable
187
186
 
188
187
 
189
188
  def cli_main(
190
- version: str, description: str, commands: dict[str, Command], test_arguments: Optional[list[str]] = None
189
+ version: str, description: str, commands: dict[str, Command], test_arguments: list[str] | None = None
191
190
  ) -> None:
192
191
  """Main entry point for the CLI.
193
192
 
@@ -17,7 +17,6 @@ from argparse import ArgumentParser
17
17
  from argparse import Namespace
18
18
  from tempfile import TemporaryDirectory
19
19
  from typing import Any
20
- from typing import Dict
21
20
 
22
21
  import yaml
23
22
 
@@ -213,7 +212,7 @@ class Metadata(Command):
213
212
  from anemoi.utils.checkpoints import load_metadata
214
213
  from anemoi.utils.checkpoints import replace_metadata
215
214
 
216
- kwargs: Dict[str, Any] = {}
215
+ kwargs: dict[str, Any] = {}
217
216
 
218
217
  if args.json:
219
218
  ext = "json"
@@ -10,15 +10,11 @@
10
10
  from __future__ import annotations
11
11
 
12
12
  import functools
13
+ from collections.abc import Callable
13
14
  from typing import Any
14
- from typing import Callable
15
- from typing import Optional
16
- from typing import Union
17
15
 
18
16
 
19
- def aliases(
20
- aliases: Optional[dict[str, Union[str, list[str]]]] = None, **kwargs: Any
21
- ) -> Callable[[Callable], Callable]:
17
+ def aliases(aliases: dict[str, str | list[str]] | None = None, **kwargs: Any) -> Callable[[Callable], Callable]:
22
18
  """Alias keyword arguments in a function call.
23
19
 
24
20
  Allows for dynamically renaming keyword arguments in a function call.
@@ -15,9 +15,8 @@ import json
15
15
  import logging
16
16
  import os
17
17
  import threading
18
+ import warnings
18
19
  from typing import Any
19
- from typing import Optional
20
- from typing import Union
21
20
 
22
21
  import yaml
23
22
 
@@ -62,14 +61,20 @@ class DotDict(dict):
62
61
  super().__init__(*args, **kwargs)
63
62
 
64
63
  for k, v in self.items():
65
- if isinstance(v, dict) or is_omegaconf_dict(v):
66
- self[k] = DotDict(v)
64
+ self[k] = self.convert_to_nested_dot_dict(v)
67
65
 
68
- if isinstance(v, list) or is_omegaconf_list(v):
69
- self[k] = [DotDict(i) if isinstance(i, dict) or is_omegaconf_dict(i) else i for i in v]
66
+ @staticmethod
67
+ def convert_to_nested_dot_dict(value):
68
+ if isinstance(value, dict) or is_omegaconf_dict(value):
69
+ return DotDict(value)
70
70
 
71
- if isinstance(v, tuple):
72
- self[k] = [DotDict(i) if isinstance(i, dict) or is_omegaconf_dict(i) else i for i in v]
71
+ if isinstance(value, list) or is_omegaconf_list(value):
72
+ return [DotDict(i) if isinstance(i, dict) or is_omegaconf_dict(i) else i for i in value]
73
+
74
+ if isinstance(value, tuple):
75
+ return [DotDict(i) if isinstance(i, dict) or is_omegaconf_dict(i) else i for i in value]
76
+
77
+ return value
73
78
 
74
79
  @classmethod
75
80
  def from_file(cls, path: str) -> DotDict:
@@ -109,7 +114,7 @@ class DotDict(dict):
109
114
  DotDict
110
115
  The created DotDict.
111
116
  """
112
- with open(path, "r") as file:
117
+ with open(path) as file:
113
118
  data = yaml.safe_load(file)
114
119
 
115
120
  return cls(data)
@@ -128,7 +133,7 @@ class DotDict(dict):
128
133
  DotDict
129
134
  The created DotDict.
130
135
  """
131
- with open(path, "r") as file:
136
+ with open(path) as file:
132
137
  data = json.load(file)
133
138
 
134
139
  return cls(data)
@@ -147,7 +152,7 @@ class DotDict(dict):
147
152
  DotDict
148
153
  The created DotDict.
149
154
  """
150
- with open(path, "r") as file:
155
+ with open(path) as file:
151
156
  data = tomllib.load(file)
152
157
  return cls(data)
153
158
 
@@ -179,9 +184,27 @@ class DotDict(dict):
179
184
  value : Any
180
185
  The attribute value.
181
186
  """
182
- if isinstance(value, dict):
183
- value = DotDict(value)
184
- self[attr] = value
187
+
188
+ self.warn_on_mutation(attr)
189
+ value = self.convert_to_nested_dot_dict(value)
190
+ super().__setitem__(attr, value)
191
+
192
+ def __setitem__(self, key: str, value: Any) -> None:
193
+ """Set an item in the dictionary.
194
+
195
+ Parameters
196
+ ----------
197
+ key : str
198
+ The key to set.
199
+ value : Any
200
+ The value to set.
201
+ """
202
+ self.warn_on_mutation(key)
203
+ value = self.convert_to_nested_dot_dict(value)
204
+ super().__setitem__(key, value)
205
+
206
+ def warn_on_mutation(self, key):
207
+ warnings.warn("Mofifying and instance of DotDict(). This class is intended to be immutable.")
185
208
 
186
209
  def __repr__(self) -> str:
187
210
  """Return a string representation of the DotDict.
@@ -243,7 +266,7 @@ QUIET = False
243
266
  CONFIG_PATCH = None
244
267
 
245
268
 
246
- def _find(config: Union[dict, list], what: str, result: list = None) -> list:
269
+ def _find(config: dict | list, what: str, result: list = None) -> list:
247
270
  """Find all occurrences of a key in a nested dictionary or list.
248
271
 
249
272
  Parameters
@@ -408,8 +431,8 @@ def load_any_dict_format(path: str) -> dict:
408
431
 
409
432
  def _load_config(
410
433
  name: str = "settings.toml",
411
- secrets: Optional[Union[str, list[str]]] = None,
412
- defaults: Optional[Union[str, dict]] = None,
434
+ secrets: str | list[str] | None = None,
435
+ defaults: str | dict | None = None,
413
436
  ) -> DotDict:
414
437
  """Load a configuration file.
415
438
 
@@ -531,8 +554,8 @@ def save_config(name: str, data: Any) -> None:
531
554
 
532
555
  def load_config(
533
556
  name: str = "settings.toml",
534
- secrets: Optional[Union[str, list[str]]] = None,
535
- defaults: Optional[Union[str, dict]] = None,
557
+ secrets: str | list[str] | None = None,
558
+ defaults: str | dict | None = None,
536
559
  ) -> DotDict | str:
537
560
  """Read a configuration file.
538
561
 
@@ -558,7 +581,7 @@ def load_config(
558
581
  return config
559
582
 
560
583
 
561
- def load_raw_config(name: str, default: Any = None) -> Union[DotDict, str]:
584
+ def load_raw_config(name: str, default: Any = None) -> DotDict | str:
562
585
  """Load a raw configuration file.
563
586
 
564
587
  Parameters
@@ -617,7 +640,7 @@ def check_config_mode(name: str = "settings.toml", secrets_name: str = None, sec
617
640
  CHECKED[name] = True
618
641
 
619
642
 
620
- def find(metadata: Union[dict, list], what: str, result: list = None, *, select: callable = None) -> list:
643
+ def find(metadata: dict | list, what: str, result: list = None, *, select: callable = None) -> list:
621
644
  """Find all occurrences of a key in a nested dictionary or list with an optional selector.
622
645
 
623
646
  Parameters
@@ -12,16 +12,11 @@ import calendar
12
12
  import datetime
13
13
  import re
14
14
  from typing import Any
15
- from typing import List
16
- from typing import Optional
17
- from typing import Set
18
- from typing import Tuple
19
- from typing import Union
20
15
 
21
16
  import aniso8601
22
17
 
23
18
 
24
- def normalise_frequency(frequency: Union[int, str]) -> int:
19
+ def normalise_frequency(frequency: int | str) -> int:
25
20
  """Normalise frequency to hours.
26
21
 
27
22
  Parameters
@@ -61,7 +56,7 @@ def _no_time_zone(date: datetime.datetime) -> datetime.datetime:
61
56
 
62
57
 
63
58
  # this function is use in anemoi-datasets
64
- def as_datetime(date: Union[datetime.date, datetime.datetime, str], keep_time_zone: bool = False) -> datetime.datetime:
59
+ def as_datetime(date: datetime.date | datetime.datetime | str, keep_time_zone: bool = False) -> datetime.datetime:
65
60
  """Convert a date to a datetime object, removing any time zone information.
66
61
 
67
62
  Parameters
@@ -91,9 +86,7 @@ def as_datetime(date: Union[datetime.date, datetime.datetime, str], keep_time_zo
91
86
  raise ValueError(f"Invalid date type: {type(date)}")
92
87
 
93
88
 
94
- def _as_datetime_list(
95
- date: Union[datetime.date, datetime.datetime, str], default_increment: datetime.timedelta
96
- ) -> iter:
89
+ def _as_datetime_list(date: datetime.date | datetime.datetime | str, default_increment: datetime.timedelta) -> iter:
97
90
  """Convert a date to a list of datetime objects.
98
91
 
99
92
  Parameters
@@ -137,7 +130,7 @@ def _as_datetime_list(
137
130
 
138
131
 
139
132
  def as_datetime_list(
140
- date: Union[datetime.date, datetime.datetime, str], default_increment: int = 1
133
+ date: datetime.date | datetime.datetime | str, default_increment: int = 1
141
134
  ) -> list[datetime.datetime]:
142
135
  """Convert a date to a list of datetime objects.
143
136
 
@@ -157,7 +150,7 @@ def as_datetime_list(
157
150
  return list(_as_datetime_list(date, default_increment))
158
151
 
159
152
 
160
- def as_timedelta(frequency: Union[int, str, datetime.timedelta]) -> datetime.timedelta:
153
+ def as_timedelta(frequency: int | str | datetime.timedelta) -> datetime.timedelta:
161
154
  """Convert anything to a timedelta object.
162
155
 
163
156
  Parameters
@@ -237,7 +230,7 @@ def as_timedelta(frequency: Union[int, str, datetime.timedelta]) -> datetime.tim
237
230
  raise ValueError(f"Cannot convert frequency {frequency} to timedelta")
238
231
 
239
232
 
240
- def frequency_to_timedelta(frequency: Union[int, str, datetime.timedelta]) -> datetime.timedelta:
233
+ def frequency_to_timedelta(frequency: int | str | datetime.timedelta) -> datetime.timedelta:
241
234
  """Convert a frequency to a timedelta object.
242
235
 
243
236
  Parameters
@@ -302,7 +295,7 @@ def frequency_to_string(frequency: datetime.timedelta) -> str:
302
295
  return str(frequency)
303
296
 
304
297
 
305
- def frequency_to_seconds(frequency: Union[int, str, datetime.timedelta]) -> int:
298
+ def frequency_to_seconds(frequency: int | str | datetime.timedelta) -> int:
306
299
  """Convert a frequency to seconds.
307
300
 
308
301
  Parameters
@@ -347,7 +340,7 @@ MONTH = {
347
340
  }
348
341
 
349
342
 
350
- def _make_day(day: Optional[Tuple[int, List[int]]]) -> Set[int]:
343
+ def _make_day(day: tuple[int, list[int]] | None) -> set[int]:
351
344
  """Create a set of days.
352
345
 
353
346
  Parameters
@@ -367,7 +360,7 @@ def _make_day(day: Optional[Tuple[int, List[int]]]) -> Set[int]:
367
360
  return {int(d) for d in day}
368
361
 
369
362
 
370
- def _make_week(week: Optional[Tuple[str, List[str]]]) -> Set[int]:
363
+ def _make_week(week: tuple[str, list[str]] | None) -> set[int]:
371
364
  """Create a set of weekdays.
372
365
 
373
366
  Parameters
@@ -387,7 +380,7 @@ def _make_week(week: Optional[Tuple[str, List[str]]]) -> Set[int]:
387
380
  return {DOW[w.lower()] for w in week}
388
381
 
389
382
 
390
- def _make_months(months: Optional[Union[int, str, List[Union[int, str]]]]) -> Set[int]:
383
+ def _make_months(months: int | str | list[int | str] | None) -> set[int]:
391
384
  """Create a set of months.
392
385
 
393
386
  Parameters
@@ -414,13 +407,13 @@ class DateTimes:
414
407
 
415
408
  def __init__(
416
409
  self,
417
- start: Union[datetime.date, datetime.datetime, str],
418
- end: Union[datetime.date, datetime.datetime, str],
410
+ start: datetime.date | datetime.datetime | str,
411
+ end: datetime.date | datetime.datetime | str,
419
412
  increment: int = 24,
420
413
  *,
421
- day_of_month: Optional[Tuple[int, List[int]]] = None,
422
- day_of_week: Optional[Tuple[str, List[str]]] = None,
423
- calendar_months: Optional[Union[int, str, List[Union[int, str]]]] = None,
414
+ day_of_month: tuple[int, list[int]] | None = None,
415
+ day_of_week: tuple[str, list[str]] | None = None,
416
+ calendar_months: int | str | list[int | str] | None = None,
424
417
  ):
425
418
  """Initialize the DateTimes iterator.
426
419
 
@@ -571,7 +564,7 @@ class ConcatDateTimes:
571
564
  class EnumDateTimes:
572
565
  """EnumDateTimes is an iterator that generates datetime objects from a list of dates."""
573
566
 
574
- def __init__(self, dates: list[Union[datetime.date, datetime.datetime, str]]):
567
+ def __init__(self, dates: list[datetime.date | datetime.datetime | str]):
575
568
  """Initialize the EnumDateTimes iterator.
576
569
 
577
570
  Parameters
@@ -593,7 +586,7 @@ class EnumDateTimes:
593
586
  yield as_datetime(date)
594
587
 
595
588
 
596
- def datetimes_factory(*args: Any, **kwargs: Any) -> Union[DateTimes, ConcatDateTimes, EnumDateTimes]:
589
+ def datetimes_factory(*args: Any, **kwargs: Any) -> DateTimes | ConcatDateTimes | EnumDateTimes:
597
590
  """Create a DateTimes, ConcatDateTimes, or EnumDateTimes object.
598
591
 
599
592
  Parameters
@@ -15,8 +15,6 @@ See https://codes.ecmwf.int/grib/param-db/ for more information.
15
15
 
16
16
  import logging
17
17
  import re
18
- from typing import Dict
19
- from typing import Union
20
18
 
21
19
  import requests
22
20
 
@@ -26,7 +24,7 @@ LOG = logging.getLogger(__name__)
26
24
 
27
25
 
28
26
  @cached(collection="grib", expires=30 * 24 * 60 * 60)
29
- def _units() -> Dict[str, str]:
27
+ def _units() -> dict[str, str]:
30
28
  """Fetch and cache GRIB parameter units.
31
29
 
32
30
  Returns
@@ -41,7 +39,7 @@ def _units() -> Dict[str, str]:
41
39
 
42
40
 
43
41
  @cached(collection="grib", expires=30 * 24 * 60 * 60)
44
- def _search_param(name: str) -> Dict[str, Union[str, int]]:
42
+ def _search_param(name: str) -> dict[str, str | int]:
45
43
  """Search for a GRIB parameter by name.
46
44
 
47
45
  Parameters
@@ -116,7 +114,7 @@ def paramid_to_shortname(paramid: int) -> str:
116
114
  return _search_param(str(paramid))["shortname"]
117
115
 
118
116
 
119
- def units(param: Union[int, str]) -> str:
117
+ def units(param: int | str) -> str:
120
118
  """Return the units of a GRIB parameter given its name or id.
121
119
 
122
120
  Parameters
@@ -137,7 +135,7 @@ def units(param: Union[int, str]) -> str:
137
135
  return _units()[unit_id]
138
136
 
139
137
 
140
- def must_be_positive(param: Union[int, str]) -> bool:
138
+ def must_be_positive(param: int | str) -> bool:
141
139
  """Check if a parameter must be positive.
142
140
 
143
141
  Parameters
@@ -13,9 +13,6 @@
13
13
  import logging
14
14
  import os
15
15
  from io import BytesIO
16
- from typing import List
17
- from typing import Tuple
18
- from typing import Union
19
16
 
20
17
  import deprecation
21
18
  import numpy as np
@@ -145,7 +142,7 @@ def nearest_grid_points(
145
142
 
146
143
 
147
144
  @cached(collection="grids", encoding="npz")
148
- def _grids(name: Union[str, List[float], Tuple[float, ...]]) -> bytes:
145
+ def _grids(name: str | list[float] | tuple[float, ...]) -> bytes:
149
146
  """Get grid data by name.
150
147
 
151
148
  Parameters
@@ -196,7 +193,7 @@ def _grids(name: Union[str, List[float], Tuple[float, ...]]) -> bytes:
196
193
  current_version=__version__,
197
194
  details="Use anemoi.transform.grids.named.lookup instead.",
198
195
  )
199
- def grids(name: Union[str, List[float], Tuple[float, ...]]) -> dict:
196
+ def grids(name: str | list[float] | tuple[float, ...]) -> dict:
200
197
  """Load grid data by name.
201
198
 
202
199
  Parameters
@@ -9,9 +9,7 @@
9
9
 
10
10
 
11
11
  import datetime
12
- from typing import Iterator
13
- from typing import List
14
- from typing import Tuple
12
+ from collections.abc import Iterator
15
13
 
16
14
 
17
15
  class HindcastDatesTimes:
@@ -25,7 +23,7 @@ class HindcastDatesTimes:
25
23
  Number of years to go back from each reference date.
26
24
  """
27
25
 
28
- def __init__(self, reference_dates: List[datetime.datetime], years: int = 20):
26
+ def __init__(self, reference_dates: list[datetime.datetime], years: int = 20):
29
27
  """Initialize the HindcastDatesTimes iterator.
30
28
 
31
29
  Parameters
@@ -41,7 +39,7 @@ class HindcastDatesTimes:
41
39
  assert years > 0, f"years must be greater than 0, got {years}"
42
40
  self.years = years
43
41
 
44
- def __iter__(self) -> Iterator[Tuple[datetime.datetime, datetime.datetime]]:
42
+ def __iter__(self) -> Iterator[tuple[datetime.datetime, datetime.datetime]]:
45
43
  """Generate tuples of past dates and their corresponding reference dates.
46
44
 
47
45
  Yields