anemoi-utils 0.4.13__tar.gz → 0.4.15__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.
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.github/workflows/downstream-ci-hpc.yml +3 -3
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.gitignore +1 -0
- anemoi_utils-0.4.15/.release-please-manifest.json +3 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/CHANGELOG.md +14 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/PKG-INFO +3 -2
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/_version.py +2 -2
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/devtools.py +4 -6
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/humanize.py +4 -10
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/provenance.py +4 -4
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/registry.py +125 -29
- anemoi_utils-0.4.15/src/anemoi/utils/testing.py +182 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi_utils.egg-info/PKG-INFO +3 -2
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi_utils.egg-info/SOURCES.txt +1 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/tests/test_remote.py +6 -0
- anemoi_utils-0.4.13/.release-please-manifest.json +0 -3
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.gitattributes +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.github/CODEOWNERS +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.github/ci-hpc-config.yml +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.github/dependabot.yml +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.github/labeler.yml +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.github/pull_request_template.md +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.github/release.yml +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.github/workflows/pr-conventional-commit.yml +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.github/workflows/pr-label-conventional-commits.yml +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.github/workflows/pr-label-file-based.yml +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.github/workflows/pr-label-public.yml +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.github/workflows/python-publish.yml +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.github/workflows/python-pull-request.yml +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.github/workflows/readthedocs-pr-update.yml +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.github/workflows/release-please.yml +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.pre-commit-config.yaml +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.readthedocs.yaml +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.release-please-config.json +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/CONTRIBUTORS.md +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/LICENSE +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/README.md +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/docs/Makefile +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/docs/_static/logo.png +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/docs/_static/style.css +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/docs/_templates/.gitkeep +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/docs/conf.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/docs/index.rst +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/docs/installing.rst +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/docs/modules/checkpoints.rst +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/docs/modules/config.rst +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/docs/modules/dates.rst +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/docs/modules/grib.rst +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/docs/modules/humanize.rst +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/docs/modules/provenance.rst +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/docs/modules/s3.rst +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/docs/modules/text.rst +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/pyproject.toml +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/setup.cfg +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/__init__.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/__main__.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/caching.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/checkpoints.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/cli.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/commands/__init__.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/commands/config.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/commands/requests.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/compatibility.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/config.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/dates.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/grib.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/grids.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/hindcasts.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/logs.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/mars/__init__.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/mars/mars.yaml +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/mars/requests.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/remote/__init__.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/remote/s3.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/remote/ssh.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/s3.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/sanitise.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/sanitize.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/text.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi/utils/timer.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi_utils.egg-info/dependency_links.txt +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi_utils.egg-info/entry_points.txt +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi_utils.egg-info/requires.txt +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/src/anemoi_utils.egg-info/top_level.txt +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/tests/test-transfer-data/directory/b/c/x +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/tests/test-transfer-data/directory/b/y +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/tests/test-transfer-data/directory/exotic filename ;^/"'[=.,#]()/303/252/303/274/303/247/303/262/342/234/205.txt" +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/tests/test-transfer-data/directory/z +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/tests/test-transfer-data/file +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/tests/test_caching.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/tests/test_compatibility.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/tests/test_dates.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/tests/test_frequency.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/tests/test_grids.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/tests/test_provenance.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/tests/test_sanetise.py +0 -0
- {anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/tests/test_utils.py +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# This workflow triggers tests on dependent packages.
|
|
2
|
-
# The dependency tree itself is defined in ecmwf
|
|
2
|
+
# The dependency tree itself is defined in ecmwf/downstream-ci/
|
|
3
3
|
name: Test downstream dependent packages on HPC
|
|
4
4
|
|
|
5
5
|
on:
|
|
@@ -38,7 +38,7 @@ jobs:
|
|
|
38
38
|
downstream-ci:
|
|
39
39
|
name: downstream-ci
|
|
40
40
|
if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }}
|
|
41
|
-
uses: ecmwf
|
|
41
|
+
uses: ecmwf/downstream-ci/.github/workflows/downstream-ci.yml@main
|
|
42
42
|
with:
|
|
43
43
|
anemoi-utils: ecmwf/anemoi-utils@${{ github.event.pull_request.head.sha || github.sha }}
|
|
44
44
|
codecov_upload: true
|
|
@@ -54,7 +54,7 @@ jobs:
|
|
|
54
54
|
# downstream-ci-hpc:
|
|
55
55
|
# name: downstream-ci-hpc
|
|
56
56
|
# if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }}
|
|
57
|
-
# uses: ecmwf
|
|
57
|
+
# uses: ecmwf/downstream-ci/.github/workflows/downstream-ci-hpc.yml@main
|
|
58
58
|
# with:
|
|
59
59
|
# anemoi-utils: ecmwf/anemoi-utils@${{ github.event.pull_request.head.sha || github.sha }}
|
|
60
60
|
# secrets: inherit
|
|
@@ -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.15](https://github.com/ecmwf/anemoi-utils/compare/0.4.14...0.4.15) (2025-03-21)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* accept hyphens in factory names ([#116](https://github.com/ecmwf/anemoi-utils/issues/116)) ([ada96e9](https://github.com/ecmwf/anemoi-utils/commit/ada96e911b592ff9d95d3a93fff5a6aa21cdebbe))
|
|
17
|
+
|
|
18
|
+
## [0.4.14](https://github.com/ecmwf/anemoi-utils/compare/0.4.13...0.4.14) (2025-03-21)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
|
|
23
|
+
* plugin support ([#110](https://github.com/ecmwf/anemoi-utils/issues/110)) ([329395a](https://github.com/ecmwf/anemoi-utils/commit/329395a5870cbf59bacb39cb5afea6b91c465b07))
|
|
24
|
+
|
|
11
25
|
## [0.4.13](https://github.com/ecmwf/anemoi-utils/compare/0.4.12...0.4.13) (2025-03-14)
|
|
12
26
|
|
|
13
27
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: anemoi-utils
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.15
|
|
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
|
|
@@ -256,3 +256,4 @@ Requires-Dist: pytest; extra == "tests"
|
|
|
256
256
|
Provides-Extra: text
|
|
257
257
|
Requires-Dist: termcolor; extra == "text"
|
|
258
258
|
Requires-Dist: wcwidth; extra == "text"
|
|
259
|
+
Dynamic: license-file
|
|
@@ -81,13 +81,11 @@ def plot_values(
|
|
|
81
81
|
ax.add_feature(cfeature.BORDERS, linestyle=":")
|
|
82
82
|
|
|
83
83
|
missing_values = np.isnan(values)
|
|
84
|
-
|
|
85
84
|
if missing_value is None:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
values = np.where(missing_values, missing_value, values)
|
|
85
|
+
min = np.nanmin(values)
|
|
86
|
+
missing_value = min - np.abs(min) * 0.001
|
|
87
|
+
|
|
88
|
+
values = np.where(missing_values, missing_value, values)
|
|
91
89
|
|
|
92
90
|
if max_value is not None:
|
|
93
91
|
values = np.where(values > max_value, max_value, values)
|
|
@@ -48,7 +48,7 @@ def bytes_to_human(n: float) -> str:
|
|
|
48
48
|
"""
|
|
49
49
|
if n < 0:
|
|
50
50
|
sign = "-"
|
|
51
|
-
n
|
|
51
|
+
n = -n
|
|
52
52
|
else:
|
|
53
53
|
sign = ""
|
|
54
54
|
|
|
@@ -411,15 +411,9 @@ def when(
|
|
|
411
411
|
if years > 1:
|
|
412
412
|
return _("%d years" % (years,))
|
|
413
413
|
|
|
414
|
-
|
|
415
|
-
if
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
d = abs(now.month - month)
|
|
419
|
-
if d >= 12:
|
|
420
|
-
return _("a year")
|
|
421
|
-
else:
|
|
422
|
-
return _("%d month%s" % (d, _plural(d)))
|
|
414
|
+
delta = abs(now - then)
|
|
415
|
+
if delta.days > 1 and delta.days < 30:
|
|
416
|
+
return _("%d days" % (delta.days,))
|
|
423
417
|
|
|
424
418
|
return "on %s %d %s %d" % (
|
|
425
419
|
DOW[then.weekday()],
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
"""Collect information about the current environment, like:
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
- The Python version
|
|
14
|
+
- The versions of the modules which are currently loaded
|
|
15
|
+
- The git information for the modules which are currently loaded from a git repository
|
|
16
|
+
- ...
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
19
|
import datetime
|
|
@@ -12,9 +12,12 @@ import importlib
|
|
|
12
12
|
import logging
|
|
13
13
|
import os
|
|
14
14
|
import sys
|
|
15
|
+
import warnings
|
|
16
|
+
from functools import cached_property
|
|
15
17
|
from typing import Any
|
|
16
18
|
from typing import Callable
|
|
17
19
|
from typing import Dict
|
|
20
|
+
from typing import List
|
|
18
21
|
from typing import Optional
|
|
19
22
|
from typing import Union
|
|
20
23
|
|
|
@@ -22,6 +25,8 @@ import entrypoints
|
|
|
22
25
|
|
|
23
26
|
LOG = logging.getLogger(__name__)
|
|
24
27
|
|
|
28
|
+
DEBUG_ANEMOI_REGISTRY = int(os.environ.get("DEBUG_ANEMOI_REGISTRY", "0"))
|
|
29
|
+
|
|
25
30
|
|
|
26
31
|
class Wrapper:
|
|
27
32
|
"""A wrapper for the registry.
|
|
@@ -55,6 +60,22 @@ class Wrapper:
|
|
|
55
60
|
return factory
|
|
56
61
|
|
|
57
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
|
+
|
|
58
79
|
_BY_KIND = {}
|
|
59
80
|
|
|
60
81
|
|
|
@@ -67,13 +88,17 @@ class Registry:
|
|
|
67
88
|
The package name.
|
|
68
89
|
key : str, optional
|
|
69
90
|
The key to use for the registry, by default "_type".
|
|
91
|
+
api_version : str, optional
|
|
92
|
+
The API version, by default '1.0.0'.
|
|
70
93
|
"""
|
|
71
94
|
|
|
72
|
-
def __init__(self, package: str, key: str = "_type"):
|
|
95
|
+
def __init__(self, package: str, key: str = "_type", api_version: str = "1.0.0"):
|
|
73
96
|
self.package = package
|
|
74
|
-
self.
|
|
97
|
+
self.__registered = {}
|
|
98
|
+
self._sources = {}
|
|
75
99
|
self.kind = package.split(".")[-1]
|
|
76
100
|
self.key = key
|
|
101
|
+
self.api_version = api_version
|
|
77
102
|
_BY_KIND[self.kind] = self
|
|
78
103
|
|
|
79
104
|
@classmethod
|
|
@@ -92,7 +117,9 @@ class Registry:
|
|
|
92
117
|
"""
|
|
93
118
|
return _BY_KIND.get(kind)
|
|
94
119
|
|
|
95
|
-
def register(
|
|
120
|
+
def register(
|
|
121
|
+
self, name: str, factory: Optional[Callable] = None, source: Optional[Any] = None
|
|
122
|
+
) -> Optional[Wrapper]:
|
|
96
123
|
"""Register a factory with the registry.
|
|
97
124
|
|
|
98
125
|
Parameters
|
|
@@ -101,19 +128,31 @@ class Registry:
|
|
|
101
128
|
The name of the factory.
|
|
102
129
|
factory : Callable, optional
|
|
103
130
|
The factory to register, by default None.
|
|
131
|
+
source : Any, optional
|
|
132
|
+
The source of the factory, by default None.
|
|
104
133
|
|
|
105
134
|
Returns
|
|
106
135
|
-------
|
|
107
136
|
Wrapper, optional
|
|
108
137
|
A wrapper if the factory is None, otherwise None.
|
|
109
138
|
"""
|
|
139
|
+
|
|
140
|
+
name = name.replace("_", "-")
|
|
141
|
+
|
|
110
142
|
if factory is None:
|
|
143
|
+
# This happens when the @register decorator is used
|
|
111
144
|
return Wrapper(name, self)
|
|
112
145
|
|
|
113
|
-
|
|
146
|
+
if source is None:
|
|
147
|
+
source = getattr(factory, "_source") if hasattr(factory, "_source") else factory
|
|
148
|
+
|
|
149
|
+
if name in self.__registered:
|
|
150
|
+
warnings.warn(f"Factory '{name}' is already registered in {self.package}")
|
|
151
|
+
warnings.warn(f"Existing: {self._sources[name]}")
|
|
152
|
+
warnings.warn(f"New: {source}")
|
|
114
153
|
|
|
115
|
-
|
|
116
|
-
|
|
154
|
+
self.__registered[name] = factory
|
|
155
|
+
self._sources[name] = source
|
|
117
156
|
|
|
118
157
|
def _load(self, file: str) -> None:
|
|
119
158
|
"""Load a module from a file.
|
|
@@ -126,8 +165,33 @@ class Registry:
|
|
|
126
165
|
name, _ = os.path.splitext(file)
|
|
127
166
|
try:
|
|
128
167
|
importlib.import_module(f".{name}", package=self.package)
|
|
129
|
-
except Exception:
|
|
130
|
-
|
|
168
|
+
except Exception as e:
|
|
169
|
+
if DEBUG_ANEMOI_REGISTRY:
|
|
170
|
+
raise
|
|
171
|
+
self._registered[name] = Error(e)
|
|
172
|
+
|
|
173
|
+
def is_registered(self, name: str) -> bool:
|
|
174
|
+
"""Check if a factory is registered.
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
name : str
|
|
179
|
+
The name of the factory.
|
|
180
|
+
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
bool
|
|
184
|
+
Whether the factory is registered.
|
|
185
|
+
"""
|
|
186
|
+
|
|
187
|
+
name = name.replace("_", "-")
|
|
188
|
+
|
|
189
|
+
ok = name in self.factories
|
|
190
|
+
if not ok:
|
|
191
|
+
LOG.error(f"Cannot find '{name}' in {self.package}")
|
|
192
|
+
for e in self.factories:
|
|
193
|
+
LOG.info(f"Registered: {e} ({self._sources.get(e)})")
|
|
194
|
+
return ok
|
|
131
195
|
|
|
132
196
|
def lookup(self, name: str, *, return_none: bool = False) -> Optional[Callable]:
|
|
133
197
|
"""Lookup a factory by name.
|
|
@@ -144,9 +208,25 @@ class Registry:
|
|
|
144
208
|
Callable, optional
|
|
145
209
|
The factory if found, otherwise None.
|
|
146
210
|
"""
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
211
|
+
|
|
212
|
+
name = name.replace("_", "-")
|
|
213
|
+
|
|
214
|
+
if return_none:
|
|
215
|
+
return self.factories.get(name)
|
|
216
|
+
|
|
217
|
+
factory = self.factories.get(name)
|
|
218
|
+
if factory is None:
|
|
219
|
+
|
|
220
|
+
LOG.error(f"Cannot find '{name}' in {self.package}")
|
|
221
|
+
for e in self.factories:
|
|
222
|
+
LOG.info(f"Registered: {e} ({self._sources.get(e)})")
|
|
223
|
+
|
|
224
|
+
raise ValueError(f"Cannot find '{name}' in {self.package}")
|
|
225
|
+
|
|
226
|
+
return factory
|
|
227
|
+
|
|
228
|
+
@cached_property
|
|
229
|
+
def factories(self) -> Dict[str, Callable]:
|
|
150
230
|
|
|
151
231
|
directory = sys.modules[self.package].__path__[0]
|
|
152
232
|
|
|
@@ -167,25 +247,41 @@ class Registry:
|
|
|
167
247
|
if file.endswith(".py"):
|
|
168
248
|
self._load(file)
|
|
169
249
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
250
|
+
bits = self.package.split(".")
|
|
251
|
+
# We assume a name like anemoi.datasets.create.sources, with kind = sources
|
|
252
|
+
assert bits[-1] == self.kind, (self.package, self.kind)
|
|
253
|
+
assert len(bits) > 1, self.package
|
|
254
|
+
|
|
255
|
+
groups = []
|
|
256
|
+
middle = bits[1:-1]
|
|
257
|
+
while True:
|
|
258
|
+
group = ".".join([bits[0], *middle, bits[-1]])
|
|
259
|
+
groups.append(group)
|
|
260
|
+
if len(middle) == 0:
|
|
261
|
+
break
|
|
262
|
+
middle.pop()
|
|
263
|
+
|
|
264
|
+
groups.reverse()
|
|
178
265
|
|
|
179
|
-
|
|
180
|
-
if return_none:
|
|
181
|
-
return None
|
|
266
|
+
LOG.debug("Loading plugins from %s", groups)
|
|
182
267
|
|
|
183
|
-
|
|
184
|
-
|
|
268
|
+
for entrypoint_group in groups:
|
|
269
|
+
for entry_point in entrypoints.get_group_all(entrypoint_group):
|
|
270
|
+
source = entry_point.distro
|
|
271
|
+
try:
|
|
272
|
+
self.register(entry_point.name, entry_point.load(), source=source)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
if DEBUG_ANEMOI_REGISTRY:
|
|
275
|
+
raise
|
|
276
|
+
self.register(entry_point.name, Error(e), source=source)
|
|
185
277
|
|
|
186
|
-
|
|
278
|
+
return self.__registered
|
|
187
279
|
|
|
188
|
-
|
|
280
|
+
@property
|
|
281
|
+
def registered(self) -> List[str]:
|
|
282
|
+
"""Get the registered factories."""
|
|
283
|
+
|
|
284
|
+
return sorted(self.factories.keys())
|
|
189
285
|
|
|
190
286
|
def create(self, name: str, *args: Any, **kwargs: Any) -> Any:
|
|
191
287
|
"""Create an instance using a factory.
|
|
@@ -204,12 +300,12 @@ class Registry:
|
|
|
204
300
|
Any
|
|
205
301
|
The created instance.
|
|
206
302
|
"""
|
|
303
|
+
|
|
304
|
+
name = name.replace("_", "-")
|
|
305
|
+
|
|
207
306
|
factory = self.lookup(name)
|
|
208
307
|
return factory(*args, **kwargs)
|
|
209
308
|
|
|
210
|
-
# def __call__(self, name: str, *args, **kwargs):
|
|
211
|
-
# return self.create(name, *args, **kwargs)
|
|
212
|
-
|
|
213
309
|
def from_config(self, config: Union[str, Dict[str, Any]], *args: Any, **kwargs: Any) -> Any:
|
|
214
310
|
"""Create an instance from a configuration.
|
|
215
311
|
|
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
import atexit
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
import tempfile
|
|
15
|
+
import threading
|
|
16
|
+
|
|
17
|
+
from multiurl import download
|
|
18
|
+
|
|
19
|
+
LOG = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
TEST_DATA_URL = "https://object-store.os-api.cci1.ecmwf.int/ml-tests/test-data/samples/"
|
|
22
|
+
|
|
23
|
+
lock = threading.RLock()
|
|
24
|
+
TEMPORARY_DIRECTORY = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _temporary_directory() -> str:
|
|
28
|
+
"""Return a temporary directory in which to download test data.
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
str
|
|
33
|
+
The path to the temporary directory.
|
|
34
|
+
"""
|
|
35
|
+
global TEMPORARY_DIRECTORY
|
|
36
|
+
with lock:
|
|
37
|
+
if TEMPORARY_DIRECTORY is not None:
|
|
38
|
+
return TEMPORARY_DIRECTORY
|
|
39
|
+
|
|
40
|
+
TEMPORARY_DIRECTORY = tempfile.mkdtemp()
|
|
41
|
+
|
|
42
|
+
# Register a cleanup function to remove the directory at exit
|
|
43
|
+
atexit.register(shutil.rmtree, TEMPORARY_DIRECTORY)
|
|
44
|
+
|
|
45
|
+
return TEMPORARY_DIRECTORY
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _check_path(path: str) -> None:
|
|
49
|
+
"""Check if the given path is normalized, not absolute, and does not start with a dot.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
path : str
|
|
54
|
+
The path to check.
|
|
55
|
+
|
|
56
|
+
Raises
|
|
57
|
+
------
|
|
58
|
+
AssertionError
|
|
59
|
+
If the path is not normalized, is absolute, or starts with a dot.
|
|
60
|
+
"""
|
|
61
|
+
assert os.path.normpath(path) == path, f"Path '{path}' should be normalized"
|
|
62
|
+
assert not os.path.isabs(path), f"Path '{path}' should not be absolute"
|
|
63
|
+
assert not path.startswith("."), f"Path '{path}' should not start with '.'"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def url_for_test_data(path: str) -> str:
|
|
67
|
+
"""Generate the URL for the test data based on the given path.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
path : str
|
|
72
|
+
The relative path to the test data.
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
str
|
|
77
|
+
The full URL to the test data.
|
|
78
|
+
"""
|
|
79
|
+
_check_path(path)
|
|
80
|
+
|
|
81
|
+
return f"{TEST_DATA_URL}{path}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_test_data(path: str, gzipped=False) -> str:
|
|
85
|
+
"""Download the test data to a temporary directory and return the local path.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
path : str
|
|
90
|
+
The relative path to the test data.
|
|
91
|
+
gzipped : bool, optional
|
|
92
|
+
Flag indicating if the remote file is gzipped, by default False. The local file will be gunzipped.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
str
|
|
97
|
+
The local path to the downloaded test data.
|
|
98
|
+
"""
|
|
99
|
+
_check_path(path)
|
|
100
|
+
|
|
101
|
+
target = os.path.normpath(os.path.join(_temporary_directory(), path))
|
|
102
|
+
with lock:
|
|
103
|
+
if os.path.exists(target):
|
|
104
|
+
return target
|
|
105
|
+
|
|
106
|
+
os.makedirs(os.path.dirname(target), exist_ok=True)
|
|
107
|
+
url = url_for_test_data(path)
|
|
108
|
+
|
|
109
|
+
if gzipped:
|
|
110
|
+
url += ".gz"
|
|
111
|
+
target += ".gz"
|
|
112
|
+
|
|
113
|
+
LOG.info(f"Downloading test data from {url} to {target}")
|
|
114
|
+
|
|
115
|
+
download(url, target)
|
|
116
|
+
|
|
117
|
+
if gzipped:
|
|
118
|
+
import gzip
|
|
119
|
+
|
|
120
|
+
with gzip.open(target, "rb") as f_in:
|
|
121
|
+
with open(target[:-3], "wb") as f_out:
|
|
122
|
+
shutil.copyfileobj(f_in, f_out)
|
|
123
|
+
os.remove(target)
|
|
124
|
+
target = target[:-3]
|
|
125
|
+
|
|
126
|
+
return target
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_test_archive(path: str, extension=".extracted") -> str:
|
|
130
|
+
"""Download an archive file (.zip, .tar, .tar.gz, .tar.bz2, .tar.xz) to a temporary directory
|
|
131
|
+
unpack it, and return the local path to the directory containing the extracted files.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
path : str
|
|
136
|
+
The relative path to the test data.
|
|
137
|
+
extension : str, optional
|
|
138
|
+
The extension to add to the extracted directory, by default '.extracted'
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
str
|
|
143
|
+
The local path to the downloaded test data.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
with lock:
|
|
147
|
+
|
|
148
|
+
archive = get_test_data(path)
|
|
149
|
+
target = archive + extension
|
|
150
|
+
|
|
151
|
+
shutil.unpack_archive(archive, os.path.dirname(target) + ".tmp")
|
|
152
|
+
os.rename(os.path.dirname(target) + ".tmp", target)
|
|
153
|
+
|
|
154
|
+
return target
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def packages_installed(*names) -> bool:
|
|
158
|
+
"""Check if all the given packages are installed.
|
|
159
|
+
|
|
160
|
+
Use this function to check if the required packages are installed before running tests.
|
|
161
|
+
|
|
162
|
+
>>> @pytest.mark.skipif(not packages_installed("foo", "bar"), reason="Packages 'foo' and 'bar' are not installed")
|
|
163
|
+
>>> def test_foo_bar() -> None:
|
|
164
|
+
>>> ...
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
names : str
|
|
169
|
+
The names of the packages to check.
|
|
170
|
+
|
|
171
|
+
Returns
|
|
172
|
+
-------
|
|
173
|
+
bool:
|
|
174
|
+
Flag indicating if all the packages are installed."
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
for name in names:
|
|
178
|
+
try:
|
|
179
|
+
__import__(name)
|
|
180
|
+
except ImportError:
|
|
181
|
+
return False
|
|
182
|
+
return True
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: anemoi-utils
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.15
|
|
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
|
|
@@ -256,3 +256,4 @@ Requires-Dist: pytest; extra == "tests"
|
|
|
256
256
|
Provides-Extra: text
|
|
257
257
|
Requires-Dist: termcolor; extra == "text"
|
|
258
258
|
Requires-Dist: wcwidth; extra == "text"
|
|
259
|
+
Dynamic: license-file
|
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
9
|
import shutil
|
|
10
|
+
import sys
|
|
10
11
|
|
|
11
12
|
import pytest
|
|
12
13
|
|
|
13
14
|
from anemoi.utils.remote import TransferMethodNotImplementedError
|
|
14
15
|
from anemoi.utils.remote import _find_transfer_class
|
|
15
16
|
from anemoi.utils.remote import transfer
|
|
17
|
+
from anemoi.utils.testing import packages_installed
|
|
16
18
|
|
|
17
19
|
IN_CI = (os.environ.get("GITHUB_WORKFLOW") is not None) or (os.environ.get("IN_CI_HPC") is not None)
|
|
18
20
|
|
|
@@ -112,6 +114,7 @@ def test_transfer_find_none(source: str, target: str) -> None:
|
|
|
112
114
|
|
|
113
115
|
|
|
114
116
|
@pytest.mark.skipif(IN_CI, reason="Test requires access to S3")
|
|
117
|
+
@pytest.mark.skipif(not packages_installed("boto3"), reason="boto3 is not installed")
|
|
115
118
|
def test_transfer_zarr_s3_to_local(tmpdir: pytest.TempPathFactory) -> None:
|
|
116
119
|
"""Test transferring a Zarr file from S3 to local.
|
|
117
120
|
|
|
@@ -132,6 +135,7 @@ def test_transfer_zarr_s3_to_local(tmpdir: pytest.TempPathFactory) -> None:
|
|
|
132
135
|
|
|
133
136
|
|
|
134
137
|
@pytest.mark.skipif(IN_CI, reason="Test requires access to S3")
|
|
138
|
+
@pytest.mark.skipif(not packages_installed("boto3"), reason="boto3 is not installed")
|
|
135
139
|
def test_transfer_zarr_local_to_s3(tmpdir: pytest.TempPathFactory) -> None:
|
|
136
140
|
"""Test transferring a Zarr file from local to S3.
|
|
137
141
|
|
|
@@ -193,6 +197,7 @@ def compare(local1: str, local2: str) -> None:
|
|
|
193
197
|
|
|
194
198
|
|
|
195
199
|
@pytest.mark.skipif(IN_CI, reason="Test requires access to S3")
|
|
200
|
+
@pytest.mark.skipif(not packages_installed("boto3"), reason="boto3 is not installed")
|
|
196
201
|
@pytest.mark.parametrize("path", ["directory/", "file"])
|
|
197
202
|
def test_transfer_local_to_s3_to_local(path: str) -> None:
|
|
198
203
|
"""Test transferring a file or directory from local to S3 and back to local.
|
|
@@ -224,6 +229,7 @@ def test_transfer_local_to_s3_to_local(path: str) -> None:
|
|
|
224
229
|
|
|
225
230
|
|
|
226
231
|
@pytest.mark.skipif(IN_CI, reason="Test requires ssh access to localhost")
|
|
232
|
+
@pytest.mark.skipif(sys.platform == "darwin", reason="Does not work on MacOS")
|
|
227
233
|
@pytest.mark.parametrize("path", ["directory", "file"])
|
|
228
234
|
@pytest.mark.parametrize("temporary_target", [True, False])
|
|
229
235
|
def test_transfer_local_to_ssh(path: str, temporary_target: bool) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{anemoi_utils-0.4.13 → anemoi_utils-0.4.15}/.github/workflows/pr-label-conventional-commits.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|