anemoi-utils 0.4.34__tar.gz → 0.4.36__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.36/.github/workflows/pr-label-ats.yml +66 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.pre-commit-config.yaml +2 -2
- anemoi_utils-0.4.36/.release-please-manifest.json +3 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/CHANGELOG.md +24 -0
- {anemoi_utils-0.4.34/src/anemoi_utils.egg-info → anemoi_utils-0.4.36}/PKG-INFO +3 -2
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/pyproject.toml +2 -1
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/_environment.py +3 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/_version.py +3 -3
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/checkpoints.py +47 -32
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/cli.py +38 -5
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/commands/transfer.py +6 -2
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/config.py +4 -9
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/logs.py +34 -6
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/mlflow/auth.py +39 -1
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/registry.py +55 -1
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/remote/__init__.py +1 -3
- anemoi_utils-0.4.36/src/anemoi/utils/remote/s3.py +735 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36/src/anemoi_utils.egg-info}/PKG-INFO +3 -2
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi_utils.egg-info/SOURCES.txt +4 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi_utils.egg-info/requires.txt +2 -1
- anemoi_utils-0.4.36/tests/test_checkpoints.py +220 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/tests/test_mlflow_auth.py +17 -0
- anemoi_utils-0.4.36/tests/test_registry.py +64 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/tests/test_remote.py +45 -23
- anemoi_utils-0.4.36/tests/test_s3.py +65 -0
- anemoi_utils-0.4.34/.release-please-manifest.json +0 -3
- anemoi_utils-0.4.34/src/anemoi/utils/remote/s3.py +0 -789
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.gitattributes +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.github/CODEOWNERS +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.github/ci-hpc-config.yml +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.github/dependabot.yml +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.github/labeler.yml +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.github/pull_request_template.md +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.github/workflows/downstream-ci-hpc.yml +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.github/workflows/pr-conventional-commit.yml +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.github/workflows/pr-label-conventional-commits.yml +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.github/workflows/pr-label-file-based.yml +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.github/workflows/pr-label-public.yml +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.github/workflows/python-publish.yml +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.github/workflows/python-pull-request.yml +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.github/workflows/readthedocs-pr-update.yml +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.github/workflows/release-please.yml +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.gitignore +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.readthedocs.yaml +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/.release-please-config.json +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/CONTRIBUTORS.md +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/LICENSE +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/README.md +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/docs/Makefile +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/docs/_static/logo.png +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/docs/_static/style.css +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/docs/_templates/.gitkeep +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/docs/_templates/apidoc/package.rst.jinja +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/docs/conf.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/docs/index.rst +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/docs/installing.rst +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/docs/modules/checkpoints.rst +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/docs/modules/config.rst +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/docs/modules/dates.rst +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/docs/modules/grib.rst +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/docs/modules/humanize.rst +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/docs/modules/provenance.rst +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/docs/modules/s3.rst +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/docs/modules/testing.rst +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/docs/modules/text.rst +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/docs/scripts/api_build.sh +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/setup.cfg +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/__init__.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/__main__.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/caching.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/commands/__init__.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/commands/config.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/commands/metadata.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/commands/requests.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/compatibility.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/dates.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/devtools.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/grib.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/grids.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/hindcasts.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/humanize.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/mars/__init__.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/mars/mars.yaml +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/mars/requests.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/mlflow/__init__.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/mlflow/client.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/mlflow/utils.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/provenance.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/remote/ssh.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/rules.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/s3.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/sanitise.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/sanitize.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/schemas/__init__.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/schemas/errors.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/testing.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/text.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi/utils/timer.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi_utils.egg-info/dependency_links.txt +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi_utils.egg-info/entry_points.txt +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/src/anemoi_utils.egg-info/top_level.txt +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/tests/test-transfer-data/directory/b/c/x +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/tests/test-transfer-data/directory/b/y +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/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.34 → anemoi_utils-0.4.36}/tests/test-transfer-data/directory/z +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/tests/test-transfer-data/file +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/tests/test_caching.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/tests/test_compatibility.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/tests/test_dates.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/tests/test_frequency.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/tests/test_mlflow_client.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/tests/test_provenance.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/tests/test_sanitise.py +0 -0
- {anemoi_utils-0.4.34 → anemoi_utils-0.4.36}/tests/test_utils.py +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# This workflow checks that the appropriate ATS labels are applied to a pull request:
|
|
2
|
+
# - "ATS Approval Not Needed": pass
|
|
3
|
+
# - "ATS Approved": pass
|
|
4
|
+
# - "ATS Approval Needed": fail
|
|
5
|
+
# - Missing ATS label: fail
|
|
6
|
+
|
|
7
|
+
name: "[PR] ATS labels"
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
pull_request:
|
|
11
|
+
types:
|
|
12
|
+
- opened
|
|
13
|
+
- edited
|
|
14
|
+
- reopened
|
|
15
|
+
- labeled
|
|
16
|
+
- unlabeled
|
|
17
|
+
- synchronize
|
|
18
|
+
|
|
19
|
+
permissions:
|
|
20
|
+
pull-requests: read
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
check:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- name: Get PR details
|
|
27
|
+
uses: actions/github-script@v7
|
|
28
|
+
id: check-pr
|
|
29
|
+
with:
|
|
30
|
+
script: |
|
|
31
|
+
const pr = await github.rest.pulls.get({
|
|
32
|
+
owner: context.repo.owner,
|
|
33
|
+
repo: context.repo.repo,
|
|
34
|
+
pull_number: context.payload.pull_request.number,
|
|
35
|
+
});
|
|
36
|
+
const labels = pr.data.labels.map(label => label.name);
|
|
37
|
+
core.setOutput('labels', JSON.stringify(labels));
|
|
38
|
+
core.setOutput('author', context.payload.pull_request.user.login);
|
|
39
|
+
|
|
40
|
+
- name: Evaluate ATS labels
|
|
41
|
+
run: |
|
|
42
|
+
AUTHOR='${{ steps.check-pr.outputs.author }}'
|
|
43
|
+
if [ "$AUTHOR" == "DeployDuck" ] || [ "$AUTHOR" == "pre-commit-ci[bot]" ]; then
|
|
44
|
+
echo "Bot PR, skipping."
|
|
45
|
+
exit 0
|
|
46
|
+
fi
|
|
47
|
+
LABELS=$(echo '${{ steps.check-pr.outputs.labels }}' | jq -r '.[]')
|
|
48
|
+
echo "Labels found:"
|
|
49
|
+
echo -e "$LABELS\n"
|
|
50
|
+
echo "Result:"
|
|
51
|
+
if echo "$LABELS" | grep -qi "ATS approval not needed"; then
|
|
52
|
+
echo "ATS approval not needed. Passing."
|
|
53
|
+
EXIT_CODE=0
|
|
54
|
+
elif echo "$LABELS" | grep -qi "ATS approved"; then
|
|
55
|
+
echo "ATS Approved. Passing."
|
|
56
|
+
EXIT_CODE=0
|
|
57
|
+
elif echo "$LABELS" | grep -qi "ATS approval needed"; then
|
|
58
|
+
echo "ATS Approval Needed. Failing."
|
|
59
|
+
EXIT_CODE=1
|
|
60
|
+
else
|
|
61
|
+
echo "No ATS approval labels found. Please assign the appropriate ATS label. Failing."
|
|
62
|
+
EXIT_CODE=1
|
|
63
|
+
fi
|
|
64
|
+
echo -e "\nFor more information on ATS labels, see:"
|
|
65
|
+
echo "https://anemoi.readthedocs.io/en/latest/contributing/guidelines.html#labelling-guidelines"
|
|
66
|
+
exit $EXIT_CODE
|
|
@@ -10,7 +10,7 @@ repos:
|
|
|
10
10
|
entry: jupyter nbconvert --ClearOutputPreprocessor.enabled=True --inplace
|
|
11
11
|
additional_dependencies: [jupyter]
|
|
12
12
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
13
|
-
rev:
|
|
13
|
+
rev: v6.0.0
|
|
14
14
|
hooks:
|
|
15
15
|
- id: check-yaml # Check YAML files for syntax errors only
|
|
16
16
|
args: [--unsafe, --allow-multiple-documents]
|
|
@@ -41,7 +41,7 @@ repos:
|
|
|
41
41
|
- --profile black
|
|
42
42
|
- --project anemoi
|
|
43
43
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
44
|
-
rev: v0.12.
|
|
44
|
+
rev: v0.12.11
|
|
45
45
|
hooks:
|
|
46
46
|
- id: ruff
|
|
47
47
|
args:
|
|
@@ -8,6 +8,30 @@ 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.36](https://github.com/ecmwf/anemoi-utils/compare/0.4.35...0.4.36) (2025-09-22)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* Add aliases to registry ([#219](https://github.com/ecmwf/anemoi-utils/issues/219)) ([37267b5](https://github.com/ecmwf/anemoi-utils/commit/37267b548556a796a01b43abb908011eeec85454))
|
|
17
|
+
* Debug imports ([#182](https://github.com/ecmwf/anemoi-utils/issues/182)) ([1eaa615](https://github.com/ecmwf/anemoi-utils/commit/1eaa61540dc9ac3d5fe82f2c91b7fc98c8bb10af))
|
|
18
|
+
* NoAuth for AML mlflow Logging ([#200](https://github.com/ecmwf/anemoi-utils/issues/200)) ([732182e](https://github.com/ecmwf/anemoi-utils/commit/732182ea5d255ba69ea2ed0a23b307d6f64aaf84))
|
|
19
|
+
* Rich logging ([#209](https://github.com/ecmwf/anemoi-utils/issues/209)) ([3c762a5](https://github.com/ecmwf/anemoi-utils/commit/3c762a593ba2dc734becc54b92984d6dc62967ac))
|
|
20
|
+
* Speedup checkpoint editing - remove compression ([#218](https://github.com/ecmwf/anemoi-utils/issues/218)) ([b49120f](https://github.com/ecmwf/anemoi-utils/commit/b49120f763b0b6ee10c805bab2aa7b973047f963))
|
|
21
|
+
* Use obstore to access s3 buckets ([#210](https://github.com/ecmwf/anemoi-utils/issues/210)) ([da380be](https://github.com/ecmwf/anemoi-utils/commit/da380be71d78274d72bd0a3859ef00b1c80e9469))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Bug Fixes
|
|
25
|
+
|
|
26
|
+
* Add missing s3 function used by datasets ([#212](https://github.com/ecmwf/anemoi-utils/issues/212)) ([30589e8](https://github.com/ecmwf/anemoi-utils/commit/30589e891fbdb1cff205f0350c63e93a725c7242))
|
|
27
|
+
|
|
28
|
+
## [0.4.35](https://github.com/ecmwf/anemoi-utils/compare/0.4.34...0.4.35) (2025-08-12)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Bug Fixes
|
|
32
|
+
|
|
33
|
+
* Config override ([#204](https://github.com/ecmwf/anemoi-utils/issues/204)) ([cdeef1e](https://github.com/ecmwf/anemoi-utils/commit/cdeef1ef95ecd3696fefc751f8a97e90fe357329))
|
|
34
|
+
|
|
11
35
|
## [0.4.34](https://github.com/ecmwf/anemoi-utils/compare/0.4.33...0.4.34) (2025-08-11)
|
|
12
36
|
|
|
13
37
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anemoi-utils
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.36
|
|
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
|
|
@@ -225,6 +225,7 @@ Requires-Python: >=3.10
|
|
|
225
225
|
License-File: LICENSE
|
|
226
226
|
Requires-Dist: aniso8601
|
|
227
227
|
Requires-Dist: deprecation
|
|
228
|
+
Requires-Dist: entrypoints
|
|
228
229
|
Requires-Dist: importlib-metadata; python_version < "3.10"
|
|
229
230
|
Requires-Dist: multiurl
|
|
230
231
|
Requires-Dist: numpy
|
|
@@ -256,7 +257,7 @@ Provides-Extra: provenance
|
|
|
256
257
|
Requires-Dist: gitpython; extra == "provenance"
|
|
257
258
|
Requires-Dist: nvsmi; extra == "provenance"
|
|
258
259
|
Provides-Extra: s3
|
|
259
|
-
Requires-Dist:
|
|
260
|
+
Requires-Dist: obstore; extra == "s3"
|
|
260
261
|
Provides-Extra: tests
|
|
261
262
|
Requires-Dist: anemoi-utils[mlflow]; extra == "tests"
|
|
262
263
|
Requires-Dist: pytest; extra == "tests"
|
|
@@ -41,6 +41,7 @@ dynamic = [ "version" ]
|
|
|
41
41
|
dependencies = [
|
|
42
42
|
"aniso8601",
|
|
43
43
|
"deprecation",
|
|
44
|
+
"entrypoints",
|
|
44
45
|
"importlib-metadata; python_version<'3.10'",
|
|
45
46
|
"multiurl",
|
|
46
47
|
"numpy",
|
|
@@ -73,7 +74,7 @@ optional-dependencies.mlflow = [ "mlflow-skinny>=2.11.1", "requests" ]
|
|
|
73
74
|
optional-dependencies.provenance = [ "gitpython", "nvsmi" ]
|
|
74
75
|
|
|
75
76
|
optional-dependencies.s3 = [
|
|
76
|
-
"
|
|
77
|
+
"obstore",
|
|
77
78
|
]
|
|
78
79
|
|
|
79
80
|
optional-dependencies.tests = [ "anemoi-utils[mlflow]", "pytest", "pytest-mock>=3" ]
|
|
@@ -17,6 +17,9 @@ class Environment:
|
|
|
17
17
|
ANEMOI_CONFIG_OVERRIDE_PATH: str
|
|
18
18
|
"""Path to the configuration override file for Anemoi."""
|
|
19
19
|
|
|
20
|
+
ANEMOI_DEBUG_IMPORTS: bool
|
|
21
|
+
"""Enable debug imports to trace module loading."""
|
|
22
|
+
|
|
20
23
|
def __setattr__(self, name: str, value: Any) -> None:
|
|
21
24
|
raise AttributeError("Cannot set attributes on Environment class. Use environment variables instead.")
|
|
22
25
|
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.4.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 4,
|
|
31
|
+
__version__ = version = '0.4.36'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 4, 36)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g67b40f99c'
|
|
@@ -214,7 +214,7 @@ def save_metadata(
|
|
|
214
214
|
zipf.writestr(entry["path"], value.tobytes())
|
|
215
215
|
|
|
216
216
|
|
|
217
|
-
def _edit_metadata(path: str, name: str, callback: Callable, supporting_arrays: dict = None) -> None:
|
|
217
|
+
def _edit_metadata(path: str, name: str, callback: Callable, supporting_arrays: dict | None = None) -> None:
|
|
218
218
|
"""Edit metadata in a checkpoint file.
|
|
219
219
|
|
|
220
220
|
Parameters
|
|
@@ -230,41 +230,56 @@ def _edit_metadata(path: str, name: str, callback: Callable, supporting_arrays:
|
|
|
230
230
|
"""
|
|
231
231
|
new_path = f"{path}.anemoi-edit-{time.time()}-{os.getpid()}.tmp"
|
|
232
232
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
for
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
directory = os.path.dirname(full)
|
|
247
|
-
|
|
248
|
-
if not found:
|
|
233
|
+
with zipfile.ZipFile(path, "r") as source_zip:
|
|
234
|
+
file_list = source_zip.namelist()
|
|
235
|
+
|
|
236
|
+
# Find the target file and its directory
|
|
237
|
+
target_file = None
|
|
238
|
+
directory = None
|
|
239
|
+
for file_path in file_list:
|
|
240
|
+
if os.path.basename(file_path) == name:
|
|
241
|
+
target_file = file_path
|
|
242
|
+
directory = os.path.dirname(file_path)
|
|
243
|
+
break
|
|
244
|
+
|
|
245
|
+
if target_file is None:
|
|
249
246
|
raise ValueError(f"Could not find '{name}' in {path}")
|
|
250
247
|
|
|
248
|
+
# Calculate total files for progress bar
|
|
249
|
+
total_files = len(file_list)
|
|
251
250
|
if supporting_arrays is not None:
|
|
251
|
+
total_files += len(supporting_arrays)
|
|
252
|
+
|
|
253
|
+
with zipfile.ZipFile(new_path, "w", zipfile.ZIP_STORED) as new_zip:
|
|
254
|
+
with tqdm.tqdm(total=total_files, desc="Rebuilding checkpoint") as pbar:
|
|
255
|
+
|
|
256
|
+
# Copy all files except the target file
|
|
257
|
+
for file_path in file_list:
|
|
258
|
+
if file_path != target_file:
|
|
259
|
+
with source_zip.open(file_path) as source_file:
|
|
260
|
+
data = source_file.read()
|
|
261
|
+
new_zip.writestr(file_path, data)
|
|
262
|
+
pbar.update(1)
|
|
252
263
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
264
|
+
# Handle the target file with callback
|
|
265
|
+
with TemporaryDirectory() as temp_dir:
|
|
266
|
+
# Extract only the target file
|
|
267
|
+
source_zip.extract(target_file, temp_dir)
|
|
268
|
+
target_full_path = os.path.join(temp_dir, target_file)
|
|
269
|
+
|
|
270
|
+
# Apply the callback
|
|
271
|
+
callback(target_full_path)
|
|
272
|
+
|
|
273
|
+
# Add the modified file to the new zip (if it still exists)
|
|
274
|
+
if os.path.exists(target_full_path):
|
|
275
|
+
new_zip.write(target_full_path, target_file)
|
|
276
|
+
pbar.update(1)
|
|
277
|
+
|
|
278
|
+
# Add supporting arrays if provided
|
|
279
|
+
if supporting_arrays is not None:
|
|
280
|
+
for key, entry in supporting_arrays.items():
|
|
281
|
+
array_path = os.path.join(directory, f"{key}.numpy") if directory else f"{key}.numpy"
|
|
282
|
+
new_zip.writestr(array_path, entry.tobytes())
|
|
268
283
|
pbar.update(1)
|
|
269
284
|
|
|
270
285
|
os.rename(new_path, path)
|
|
@@ -16,11 +16,26 @@ import sys
|
|
|
16
16
|
import traceback
|
|
17
17
|
from collections.abc import Callable
|
|
18
18
|
|
|
19
|
+
from anemoi.utils import ENV
|
|
20
|
+
|
|
19
21
|
try:
|
|
20
22
|
import argcomplete
|
|
21
23
|
except ImportError:
|
|
22
24
|
argcomplete = None
|
|
23
25
|
|
|
26
|
+
|
|
27
|
+
if ENV.ANEMOI_DEBUG_IMPORTS:
|
|
28
|
+
from datetime import datetime
|
|
29
|
+
from importlib.abc import MetaPathFinder
|
|
30
|
+
|
|
31
|
+
class ImportTracer(MetaPathFinder):
|
|
32
|
+
def find_spec(self, fullname, path, target=None):
|
|
33
|
+
now = datetime.now().isoformat(timespec="milliseconds")
|
|
34
|
+
print(f"[{now}] Importing {fullname} from {path}")
|
|
35
|
+
return None # allow normal import processing to continue
|
|
36
|
+
|
|
37
|
+
sys.meta_path.insert(0, ImportTracer())
|
|
38
|
+
|
|
24
39
|
LOG = logging.getLogger(__name__)
|
|
25
40
|
|
|
26
41
|
|
|
@@ -29,6 +44,10 @@ class Command:
|
|
|
29
44
|
|
|
30
45
|
accept_unknown_args = False
|
|
31
46
|
|
|
47
|
+
def check(self, parser: argparse.ArgumentParser, args: argparse.Namespace) -> None:
|
|
48
|
+
"""Check the command arguments."""
|
|
49
|
+
pass
|
|
50
|
+
|
|
32
51
|
def run(self, args: argparse.Namespace) -> None:
|
|
33
52
|
"""Run the command.
|
|
34
53
|
|
|
@@ -72,6 +91,11 @@ def make_parser(description: str, commands: dict[str, Command]) -> argparse.Argu
|
|
|
72
91
|
action="store_true",
|
|
73
92
|
help="Debug mode",
|
|
74
93
|
)
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"--rich",
|
|
96
|
+
action="store_true",
|
|
97
|
+
help="Use rich for logging",
|
|
98
|
+
)
|
|
75
99
|
|
|
76
100
|
subparsers = parser.add_subparsers(help="commands:", dest="command")
|
|
77
101
|
for name, command in commands.items():
|
|
@@ -216,16 +240,25 @@ def cli_main(
|
|
|
216
240
|
|
|
217
241
|
cmd = commands[args.command]
|
|
218
242
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
243
|
+
if args.rich:
|
|
244
|
+
from .logs import get_rich_handler
|
|
245
|
+
|
|
246
|
+
logging.basicConfig(
|
|
247
|
+
format="%(message)s", level=logging.DEBUG if args.debug else logging.INFO, handlers=[get_rich_handler()]
|
|
248
|
+
)
|
|
249
|
+
else:
|
|
250
|
+
logging.basicConfig(
|
|
251
|
+
format="%(asctime)s %(levelname)s %(message)s",
|
|
252
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
253
|
+
level=logging.DEBUG if args.debug else logging.INFO,
|
|
254
|
+
)
|
|
224
255
|
|
|
225
256
|
if unknown and not cmd.accept_unknown_args:
|
|
226
257
|
# This should trigger an error
|
|
227
258
|
parser.parse_args(test_arguments)
|
|
228
259
|
|
|
260
|
+
cmd.check(parser, args)
|
|
261
|
+
|
|
229
262
|
try:
|
|
230
263
|
if unknown:
|
|
231
264
|
cmd.run(args, unknown)
|
|
@@ -28,10 +28,14 @@ class Transfer(Command):
|
|
|
28
28
|
The argument parser to which the arguments will be added.
|
|
29
29
|
"""
|
|
30
30
|
command_parser.add_argument(
|
|
31
|
-
"--source",
|
|
31
|
+
"--source",
|
|
32
|
+
help="A path to a local file or folder or a URL to a file or a folder on S3.",
|
|
33
|
+
required=True,
|
|
32
34
|
)
|
|
33
35
|
command_parser.add_argument(
|
|
34
|
-
"--target",
|
|
36
|
+
"--target",
|
|
37
|
+
help="A path to a local file or folder or a URL to a file or a folder on S3 or a remote folder.",
|
|
38
|
+
required=True,
|
|
35
39
|
)
|
|
36
40
|
command_parser.add_argument(
|
|
37
41
|
"--overwrite",
|
|
@@ -15,7 +15,6 @@ import json
|
|
|
15
15
|
import logging
|
|
16
16
|
import os
|
|
17
17
|
import threading
|
|
18
|
-
import warnings
|
|
19
18
|
from typing import Any
|
|
20
19
|
|
|
21
20
|
import yaml
|
|
@@ -187,7 +186,6 @@ class DotDict(dict):
|
|
|
187
186
|
The attribute value.
|
|
188
187
|
"""
|
|
189
188
|
|
|
190
|
-
self.warn_on_mutation(attr)
|
|
191
189
|
value = self.convert_to_nested_dot_dict(value)
|
|
192
190
|
super().__setitem__(attr, value)
|
|
193
191
|
|
|
@@ -201,13 +199,9 @@ class DotDict(dict):
|
|
|
201
199
|
value : Any
|
|
202
200
|
The value to set.
|
|
203
201
|
"""
|
|
204
|
-
self.warn_on_mutation(key)
|
|
205
202
|
value = self.convert_to_nested_dot_dict(value)
|
|
206
203
|
super().__setitem__(key, value)
|
|
207
204
|
|
|
208
|
-
def warn_on_mutation(self, key):
|
|
209
|
-
warnings.warn("Modifying an instance of DotDict(). This class is intended to be immutable.")
|
|
210
|
-
|
|
211
205
|
def __repr__(self) -> str:
|
|
212
206
|
"""Return a string representation of the DotDict.
|
|
213
207
|
|
|
@@ -486,6 +480,10 @@ def _load_config(
|
|
|
486
480
|
secret_config = _load_config(secret_name)
|
|
487
481
|
_merge_dicts(config, secret_config)
|
|
488
482
|
|
|
483
|
+
if ENV.ANEMOI_CONFIG_OVERRIDE_PATH is not None:
|
|
484
|
+
override_config = load_any_dict_format(os.path.abspath(ENV.ANEMOI_CONFIG_OVERRIDE_PATH))
|
|
485
|
+
config = merge_configs(config, override_config)
|
|
486
|
+
|
|
489
487
|
for env, value in os.environ.items():
|
|
490
488
|
|
|
491
489
|
if not env.startswith("ANEMOI_CONFIG_"):
|
|
@@ -578,9 +576,6 @@ def load_config(
|
|
|
578
576
|
|
|
579
577
|
with CONFIG_LOCK:
|
|
580
578
|
config = _load_config(name, secrets, defaults)
|
|
581
|
-
if ENV.ANEMOI_CONFIG_OVERRIDE_PATH is not None:
|
|
582
|
-
override_config = _load_config(os.path.abspath(ENV.ANEMOI_CONFIG_OVERRIDE_PATH))
|
|
583
|
-
merge_configs(config, override_config)
|
|
584
579
|
if CONFIG_PATCH is not None:
|
|
585
580
|
config = CONFIG_PATCH(config)
|
|
586
581
|
return config
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
"""Logging utilities."""
|
|
12
12
|
|
|
13
|
+
import contextvars
|
|
13
14
|
import logging
|
|
14
|
-
import threading
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
LOGGING_NAME = contextvars.ContextVar("logging_name", default="main")
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
LOGGER = logging.getLogger(__name__)
|
|
@@ -27,7 +27,7 @@ def set_logging_name(name: str) -> None:
|
|
|
27
27
|
name : str
|
|
28
28
|
The name to set for logging.
|
|
29
29
|
"""
|
|
30
|
-
|
|
30
|
+
LOGGING_NAME.set(name)
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
class ThreadCustomFormatter(logging.Formatter):
|
|
@@ -46,7 +46,7 @@ class ThreadCustomFormatter(logging.Formatter):
|
|
|
46
46
|
str
|
|
47
47
|
The formatted log record.
|
|
48
48
|
"""
|
|
49
|
-
record.logging_name =
|
|
49
|
+
record.logging_name = LOGGING_NAME.get()
|
|
50
50
|
return super().format(record)
|
|
51
51
|
|
|
52
52
|
|
|
@@ -58,11 +58,39 @@ def enable_logging_name(name: str = "main") -> None:
|
|
|
58
58
|
name : str, optional
|
|
59
59
|
The default logging name to set, by default "main".
|
|
60
60
|
"""
|
|
61
|
-
thread_local.logging_name = name
|
|
62
61
|
|
|
63
|
-
|
|
62
|
+
logger = logging.getLogger()
|
|
63
|
+
is_rich = any(handler.__class__.__name__ == "CustomRichHandler" for handler in logger.handlers)
|
|
64
|
+
|
|
65
|
+
set_logging_name(name)
|
|
66
|
+
|
|
67
|
+
if is_rich:
|
|
68
|
+
formatter = ThreadCustomFormatter("%(message)s")
|
|
69
|
+
else:
|
|
70
|
+
formatter = ThreadCustomFormatter("%(asctime)s - [%(logging_name)s] - %(levelname)s - %(message)s")
|
|
64
71
|
|
|
65
72
|
logger = logging.getLogger()
|
|
66
73
|
|
|
67
74
|
for handler in logger.handlers:
|
|
68
75
|
handler.setFormatter(formatter)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_rich_handler() -> logging.Handler:
|
|
79
|
+
"""Return a RichHandler with custom formatting for logging."""
|
|
80
|
+
|
|
81
|
+
from rich.logging import RichHandler
|
|
82
|
+
from rich.text import Text
|
|
83
|
+
|
|
84
|
+
class CustomRichHandler(RichHandler):
|
|
85
|
+
def render_message(self, record, message):
|
|
86
|
+
global width
|
|
87
|
+
|
|
88
|
+
text = super().render_message(record, message)
|
|
89
|
+
|
|
90
|
+
if hasattr(record, "logging_name"):
|
|
91
|
+
name = record.logging_name
|
|
92
|
+
text = Text.assemble(f"[{name}]", (" → ", "dim"), text)
|
|
93
|
+
|
|
94
|
+
return text
|
|
95
|
+
|
|
96
|
+
return CustomRichHandler(log_time_format="[%X]")
|
|
@@ -13,6 +13,8 @@ from __future__ import annotations
|
|
|
13
13
|
import logging
|
|
14
14
|
import os
|
|
15
15
|
import time
|
|
16
|
+
from abc import ABC
|
|
17
|
+
from abc import abstractmethod
|
|
16
18
|
from datetime import datetime
|
|
17
19
|
from datetime import timezone
|
|
18
20
|
from functools import wraps
|
|
@@ -35,7 +37,43 @@ if TYPE_CHECKING:
|
|
|
35
37
|
from collections.abc import Callable
|
|
36
38
|
|
|
37
39
|
|
|
38
|
-
class
|
|
40
|
+
class AuthBase(ABC):
|
|
41
|
+
"""Base class for authentication implementations."""
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def __init__(self, *args, **kwargs):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def save(self, **kwargs):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def login(self, force_credentials: bool = False, **kwargs):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def authenticate(self, **kwargs):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class NoAuth(AuthBase):
|
|
61
|
+
"""No-op authentication class."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, *args, **kwargs):
|
|
64
|
+
self._enabled = False
|
|
65
|
+
|
|
66
|
+
def save(self, **kwargs):
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
def login(self, force_credentials: bool = False, **kwargs):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
def authenticate(self, **kwargs):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TokenAuth(AuthBase):
|
|
39
77
|
"""Manage authentication with a keycloak token server."""
|
|
40
78
|
|
|
41
79
|
config_file = "mlflow-token.json"
|
|
@@ -97,6 +97,8 @@ class Registry(Generic[T]):
|
|
|
97
97
|
self.package = package
|
|
98
98
|
self.__registered = {}
|
|
99
99
|
self._sources = {}
|
|
100
|
+
self._aliases = {}
|
|
101
|
+
self._warnings = set()
|
|
100
102
|
self.kind = package.split(".")[-1]
|
|
101
103
|
self.key = key
|
|
102
104
|
self.api_version = api_version
|
|
@@ -118,7 +120,9 @@ class Registry(Generic[T]):
|
|
|
118
120
|
"""
|
|
119
121
|
return _BY_KIND.get(kind)
|
|
120
122
|
|
|
121
|
-
def register(
|
|
123
|
+
def register(
|
|
124
|
+
self, name: str, factory: Callable | None = None, source: Any | None = None, aliases: list[str] | None = None
|
|
125
|
+
) -> Wrapper | None:
|
|
122
126
|
"""Register a factory with the registry.
|
|
123
127
|
|
|
124
128
|
Parameters
|
|
@@ -129,6 +133,8 @@ class Registry(Generic[T]):
|
|
|
129
133
|
The factory to register, by default None.
|
|
130
134
|
source : Any, optional
|
|
131
135
|
The source of the factory, by default None.
|
|
136
|
+
aliases : list of str, optional
|
|
137
|
+
Aliases for the factory, by default None.
|
|
132
138
|
|
|
133
139
|
Returns
|
|
134
140
|
-------
|
|
@@ -136,7 +142,13 @@ class Registry(Generic[T]):
|
|
|
136
142
|
A wrapper if the factory is None, otherwise None.
|
|
137
143
|
"""
|
|
138
144
|
|
|
145
|
+
aliases = aliases or []
|
|
146
|
+
|
|
139
147
|
name = name.replace("_", "-")
|
|
148
|
+
assert (
|
|
149
|
+
name not in self._aliases
|
|
150
|
+
), f"'{name}' is already registered for '{self._aliases[name]}' in {self.package}"
|
|
151
|
+
assert name not in aliases, f"'{name}' cannot be an alias for itself in {self.package}"
|
|
140
152
|
|
|
141
153
|
if factory is None:
|
|
142
154
|
# This happens when the @register decorator is used
|
|
@@ -150,6 +162,15 @@ class Registry(Generic[T]):
|
|
|
150
162
|
warnings.warn(f"Existing: {self._sources[name]}")
|
|
151
163
|
warnings.warn(f"New: {source}")
|
|
152
164
|
|
|
165
|
+
for alias in aliases:
|
|
166
|
+
assert (
|
|
167
|
+
alias not in self.__registered
|
|
168
|
+
), f"Alias '{alias}' is already registered as a factory in {self.package}"
|
|
169
|
+
alias = alias.replace("_", "-")
|
|
170
|
+
if alias in self._aliases:
|
|
171
|
+
warnings.warn(f"Alias '{alias}' is already registered for '{self._aliases[alias]}' in {self.package}")
|
|
172
|
+
self._aliases[alias] = name
|
|
173
|
+
|
|
153
174
|
self.__registered[name] = factory
|
|
154
175
|
self._sources[name] = source
|
|
155
176
|
|
|
@@ -188,6 +209,7 @@ class Registry(Generic[T]):
|
|
|
188
209
|
"""
|
|
189
210
|
|
|
190
211
|
name = name.replace("_", "-")
|
|
212
|
+
name = self._unalias(name)
|
|
191
213
|
|
|
192
214
|
ok = name in self.factories
|
|
193
215
|
if not ok:
|
|
@@ -213,6 +235,7 @@ class Registry(Generic[T]):
|
|
|
213
235
|
"""
|
|
214
236
|
|
|
215
237
|
name = name.replace("_", "-")
|
|
238
|
+
name = self._unalias(name)
|
|
216
239
|
|
|
217
240
|
if return_none:
|
|
218
241
|
return self.factories.get(name)
|
|
@@ -305,6 +328,7 @@ class Registry(Generic[T]):
|
|
|
305
328
|
"""
|
|
306
329
|
|
|
307
330
|
name = name.replace("_", "-")
|
|
331
|
+
name = self._unalias(name)
|
|
308
332
|
|
|
309
333
|
factory = self.lookup(name)
|
|
310
334
|
return factory(*args, **kwargs)
|
|
@@ -352,3 +376,33 @@ class Registry(Generic[T]):
|
|
|
352
376
|
raise ValueError(
|
|
353
377
|
f"Entry '{config}' must either be a string, a dictionary with a single entry, or a dictionary with a '{self.key}' key"
|
|
354
378
|
)
|
|
379
|
+
|
|
380
|
+
def _unalias(self, name: str) -> str:
|
|
381
|
+
"""Resolve an alias to its canonical name.
|
|
382
|
+
|
|
383
|
+
Parameters
|
|
384
|
+
----------
|
|
385
|
+
name : str
|
|
386
|
+
The name to resolve.
|
|
387
|
+
|
|
388
|
+
Returns
|
|
389
|
+
-------
|
|
390
|
+
str
|
|
391
|
+
The canonical name.
|
|
392
|
+
"""
|
|
393
|
+
canonical = self._aliases.get(name, name)
|
|
394
|
+
if canonical != name:
|
|
395
|
+
warnings.warn(
|
|
396
|
+
f"Alias '{name}' for '{canonical}' in {self.package} is deprecated and will be removed in a future version.",
|
|
397
|
+
category=DeprecationWarning,
|
|
398
|
+
# stacklevel=2,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
return canonical
|
|
402
|
+
|
|
403
|
+
def aliases(self):
|
|
404
|
+
"""Get the aliases."""
|
|
405
|
+
result = {}
|
|
406
|
+
for alias, name in self._aliases.items():
|
|
407
|
+
result.setdefault(name, []).append(alias)
|
|
408
|
+
return result
|