anemoi-utils 0.4.10__tar.gz → 0.4.11__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.11/.github/release.yml +20 -0
- anemoi_utils-0.4.11/.github/workflows/merge-main-into-develop.yml +31 -0
- anemoi_utils-0.4.11/.github/workflows/pr-conventional-commit.yml +21 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/.github/workflows/python-publish.yml +0 -14
- anemoi_utils-0.4.11/.github/workflows/release-please.yml +24 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/.gitignore +0 -1
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/.pre-commit-config.yaml +2 -2
- anemoi_utils-0.4.11/.release-please-config.json +19 -0
- anemoi_utils-0.4.11/.release-please-manifest.json +3 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/CHANGELOG.md +19 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/PKG-INFO +3 -2
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/pyproject.toml +1 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/_version.py +2 -2
- anemoi_utils-0.4.11/src/anemoi/utils/caching.py +124 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/config.py +17 -0
- anemoi_utils-0.4.11/src/anemoi/utils/devtools.py +83 -0
- anemoi_utils-0.4.11/src/anemoi/utils/grids.py +97 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/humanize.py +33 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi_utils.egg-info/PKG-INFO +3 -2
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi_utils.egg-info/SOURCES.txt +10 -2
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi_utils.egg-info/requires.txt +1 -0
- anemoi_utils-0.4.11/tests/test_caching.py +113 -0
- anemoi_utils-0.4.11/tests/test_grids.py +28 -0
- anemoi_utils-0.4.10/.github/workflows/changelog-pr-update.yml +0 -18
- anemoi_utils-0.4.10/.github/workflows/changelog-release-update.yml +0 -45
- anemoi_utils-0.4.10/src/anemoi/utils/caching.py +0 -74
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/.gitattributes +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/.github/CODEOWNERS +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/.github/ci-hpc-config.yml +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/.github/workflows/ci.yml +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/.github/workflows/label-public-pr.yml +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/.github/workflows/python-pull-request.yml +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/.github/workflows/readthedocs-pr-update.yml +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/.readthedocs.yaml +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/CONTRIBUTORS.md +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/LICENSE +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/README.md +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/docs/Makefile +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/docs/_static/logo.png +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/docs/_static/style.css +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/docs/_templates/.gitkeep +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/docs/conf.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/docs/index.rst +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/docs/installing.rst +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/docs/modules/checkpoints.rst +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/docs/modules/config.rst +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/docs/modules/dates.rst +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/docs/modules/grib.rst +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/docs/modules/humanize.rst +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/docs/modules/provenance.rst +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/docs/modules/s3.rst +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/docs/modules/text.rst +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/setup.cfg +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/__init__.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/__main__.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/checkpoints.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/cli.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/commands/__init__.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/commands/config.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/commands/requests.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/compatibility.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/dates.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/grib.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/hindcasts.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/logs.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/mars/__init__.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/mars/mars.yaml +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/mars/requests.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/provenance.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/registry.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/remote/__init__.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/remote/s3.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/remote/ssh.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/s3.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/sanitise.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/sanitize.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/text.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi/utils/timer.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi_utils.egg-info/dependency_links.txt +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi_utils.egg-info/entry_points.txt +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/src/anemoi_utils.egg-info/top_level.txt +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/tests/test-transfer-data/directory/b/c/x +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/tests/test-transfer-data/directory/b/y +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/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.10 → anemoi_utils-0.4.11}/tests/test-transfer-data/directory/z +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/tests/test-transfer-data/file +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/tests/test_compatibility.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/tests/test_dates.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/tests/test_frequency.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/tests/test_provenance.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/tests/test_remote.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/tests/test_sanetise.py +0 -0
- {anemoi_utils-0.4.10 → anemoi_utils-0.4.11}/tests/test_utils.py +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# .github/release.yml
|
|
2
|
+
# https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes
|
|
3
|
+
|
|
4
|
+
changelog:
|
|
5
|
+
exclude:
|
|
6
|
+
labels:
|
|
7
|
+
- ignore-for-release
|
|
8
|
+
- no-changelog
|
|
9
|
+
authors:
|
|
10
|
+
- pre-commit-ci
|
|
11
|
+
categories:
|
|
12
|
+
- title: Breaking Changes 🛠
|
|
13
|
+
labels:
|
|
14
|
+
- breaking-change
|
|
15
|
+
- title: Exciting New Features 🎉
|
|
16
|
+
labels:
|
|
17
|
+
- enhancement
|
|
18
|
+
- title: Other Changes
|
|
19
|
+
labels:
|
|
20
|
+
- "*"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Merge main into develop
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- main
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
# Needed to read branches
|
|
10
|
+
contents: read
|
|
11
|
+
# Needed to create PR's
|
|
12
|
+
pull-requests: write
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
sync-branches:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
- name: Set up Node
|
|
21
|
+
uses: actions/setup-node@v4
|
|
22
|
+
with:
|
|
23
|
+
node-version: 20
|
|
24
|
+
- name: Opening pull request
|
|
25
|
+
id: pull
|
|
26
|
+
uses: jdtx0/branch-sync@1.5.1
|
|
27
|
+
with:
|
|
28
|
+
GITHUB_TOKEN: ${{ secrets.REPO_SYNC_ACTION_PAT }}
|
|
29
|
+
FROM_BRANCH: "main"
|
|
30
|
+
TO_BRANCH: "develop"
|
|
31
|
+
PULL_REQUEST_AUTO_MERGE_METHOD: "merge"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: "Ensure Conventional Commit in PR title"
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request_target:
|
|
5
|
+
types:
|
|
6
|
+
- opened
|
|
7
|
+
- edited
|
|
8
|
+
- synchronize
|
|
9
|
+
- reopened
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
pull-requests: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
main:
|
|
16
|
+
name: Validate PR title
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: amannn/action-semantic-pull-request@v5
|
|
20
|
+
env:
|
|
21
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -8,20 +8,6 @@ on:
|
|
|
8
8
|
types: [created]
|
|
9
9
|
|
|
10
10
|
jobs:
|
|
11
|
-
quality:
|
|
12
|
-
uses: ecmwf-actions/reusable-workflows/.github/workflows/qa-precommit-run.yml@v2
|
|
13
|
-
with:
|
|
14
|
-
skip-hooks: "no-commit-to-branch"
|
|
15
|
-
|
|
16
|
-
checks:
|
|
17
|
-
strategy:
|
|
18
|
-
matrix:
|
|
19
|
-
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
|
20
|
-
uses: ecmwf-actions/reusable-workflows/.github/workflows/qa-pytest-pyproject.yml@v2
|
|
21
|
-
with:
|
|
22
|
-
python-version: ${{ matrix.python-version }}
|
|
23
|
-
|
|
24
11
|
deploy:
|
|
25
|
-
needs: [checks, quality]
|
|
26
12
|
uses: ecmwf-actions/reusable-workflows/.github/workflows/cd-pypi.yml@v2
|
|
27
13
|
secrets: inherit
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Run Release Please
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- develop
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
pull-requests: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
release-please:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: googleapis/release-please-action@v4
|
|
16
|
+
with:
|
|
17
|
+
# this assumes that you have created a personal access token
|
|
18
|
+
# (PAT) and configured it as a GitHub action secret named
|
|
19
|
+
# `MY_RELEASE_PLEASE_TOKEN` (this secret name is not important).
|
|
20
|
+
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
|
|
21
|
+
# optional. customize path to .release-please-config.json
|
|
22
|
+
config-file: .release-please-config.json
|
|
23
|
+
# Currently releases are done "from main" to have a stable branch
|
|
24
|
+
# target-branch: main
|
|
@@ -40,7 +40,7 @@ repos:
|
|
|
40
40
|
- --force-single-line-imports
|
|
41
41
|
- --profile black
|
|
42
42
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
43
|
-
rev: v0.8.
|
|
43
|
+
rev: v0.8.6
|
|
44
44
|
hooks:
|
|
45
45
|
- id: ruff
|
|
46
46
|
args:
|
|
@@ -64,7 +64,7 @@ repos:
|
|
|
64
64
|
hooks:
|
|
65
65
|
- id: pyproject-fmt
|
|
66
66
|
- repo: https://github.com/jshwi/docsig # Check docstrings against function sig
|
|
67
|
-
rev: v0.
|
|
67
|
+
rev: v0.66.1
|
|
68
68
|
hooks:
|
|
69
69
|
- id: docsig
|
|
70
70
|
args:
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"release-type": "python",
|
|
3
|
+
"bump-minor-pre-major": true,
|
|
4
|
+
"bump-patch-for-minor-pre-major": true,
|
|
5
|
+
"separate-pull-requests": true,
|
|
6
|
+
"always-update": true,
|
|
7
|
+
"changelog-type": "github",
|
|
8
|
+
"include-component-in-tag": false,
|
|
9
|
+
"include-v-in-tag": false,
|
|
10
|
+
"pull-request-title-pattern": "chore${scope}: Preparing Next Release for ${component} ${version}",
|
|
11
|
+
"pull-request-header": ":robot:This is an automated PR using `release-please`.\n\nThe following changes will be included in the release, once the next version is triggered.\n\nContent of the next Release",
|
|
12
|
+
"pull-request-footer": "> [!IMPORTANT]\n> Merging this PR creates a release. Please refer to the [documentation](https://github.com/googleapis/release-please) if you're unsure.",
|
|
13
|
+
"packages": {
|
|
14
|
+
".": {
|
|
15
|
+
"package-name": "anemoi-utils"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json"
|
|
19
|
+
}
|
|
@@ -8,6 +8,25 @@ 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.11 (2025-01-17)
|
|
12
|
+
|
|
13
|
+
<!-- Release notes generated using configuration in .github/release.yml at develop -->
|
|
14
|
+
|
|
15
|
+
## What's Changed
|
|
16
|
+
### Other Changes
|
|
17
|
+
* Feature request: Add option to read configuration from stdin by @mpartio in https://github.com/ecmwf/anemoi-utils/pull/59
|
|
18
|
+
* feat(plots): Add quick map plot for debugging by @b8raoult in https://github.com/ecmwf/anemoi-utils/pull/69
|
|
19
|
+
* feat: added-anemoi-utils-grids-and-tests by @floriankrb in https://github.com/ecmwf/anemoi-utils/pull/74
|
|
20
|
+
* feat(plot): added plotting options by @NRaoult in https://github.com/ecmwf/anemoi-utils/pull/72
|
|
21
|
+
* ci(release): Simplify Release Workflow to Minimum by @JesperDramsch in https://github.com/ecmwf/anemoi-utils/pull/78
|
|
22
|
+
* feat: adding-tools-for-grids by @floriankrb in https://github.com/ecmwf/anemoi-utils/pull/76
|
|
23
|
+
|
|
24
|
+
## New Contributors
|
|
25
|
+
* @mpartio made their first contribution in https://github.com/ecmwf/anemoi-utils/pull/59
|
|
26
|
+
* @NRaoult made their first contribution in https://github.com/ecmwf/anemoi-utils/pull/72
|
|
27
|
+
|
|
28
|
+
**Full Changelog**: https://github.com/ecmwf/anemoi-utils/compare/0.4.10...0.4.11
|
|
29
|
+
|
|
11
30
|
## [0.4.5](https://github.com/ecmwf/anemoi-utils/compare/0.4.4...0.4.5) - 2024-11-06
|
|
12
31
|
|
|
13
32
|
### What's Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: anemoi-utils
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.11
|
|
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
|
|
@@ -226,6 +226,7 @@ Requires-Python: >=3.9
|
|
|
226
226
|
License-File: LICENSE
|
|
227
227
|
Requires-Dist: aniso8601
|
|
228
228
|
Requires-Dist: importlib-metadata; python_version < "3.10"
|
|
229
|
+
Requires-Dist: numpy
|
|
229
230
|
Requires-Dist: python-dateutil
|
|
230
231
|
Requires-Dist: pyyaml
|
|
231
232
|
Requires-Dist: tomli; python_version < "3.11"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# (C) Copyright 2024 Anemoi contributors.
|
|
2
|
+
#
|
|
3
|
+
# This software is licensed under the terms of the Apache Licence Version 2.0
|
|
4
|
+
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
|
|
5
|
+
#
|
|
6
|
+
# In applying this licence, ECMWF does not waive the privileges and immunities
|
|
7
|
+
# granted to it by virtue of its status as an intergovernmental organisation
|
|
8
|
+
# nor does it submit to any jurisdiction.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
from threading import Lock
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
LOCK = Lock()
|
|
20
|
+
CACHE = {}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_cache_path(collection):
|
|
24
|
+
return os.path.join(os.path.expanduser("~"), ".cache", "anemoi", collection)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def clean_cache(collection="default"):
|
|
28
|
+
global CACHE
|
|
29
|
+
CACHE = {}
|
|
30
|
+
path = _get_cache_path(collection)
|
|
31
|
+
if not os.path.exists(path):
|
|
32
|
+
return
|
|
33
|
+
for filename in os.listdir(path):
|
|
34
|
+
os.remove(os.path.join(path, filename))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Cacher:
|
|
38
|
+
"""This class implements a simple caching mechanism.
|
|
39
|
+
Private class, do not use directly"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, collection, expires):
|
|
42
|
+
self.collection = collection
|
|
43
|
+
self.expires = expires
|
|
44
|
+
|
|
45
|
+
def __call__(self, func):
|
|
46
|
+
|
|
47
|
+
full = f"{func.__module__}.{func.__name__}"
|
|
48
|
+
|
|
49
|
+
def wrapped(*args, **kwargs):
|
|
50
|
+
with LOCK:
|
|
51
|
+
return self.cache(
|
|
52
|
+
(full, args, kwargs),
|
|
53
|
+
lambda: func(*args, **kwargs),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return wrapped
|
|
57
|
+
|
|
58
|
+
def cache(self, key, proc):
|
|
59
|
+
|
|
60
|
+
key = json.dumps(key, sort_keys=True)
|
|
61
|
+
m = hashlib.md5()
|
|
62
|
+
m.update(key.encode("utf-8"))
|
|
63
|
+
m = m.hexdigest()
|
|
64
|
+
|
|
65
|
+
if m in CACHE:
|
|
66
|
+
return CACHE[m]
|
|
67
|
+
|
|
68
|
+
path = _get_cache_path(self.collection)
|
|
69
|
+
|
|
70
|
+
filename = os.path.join(path, m) + self.ext
|
|
71
|
+
if os.path.exists(filename):
|
|
72
|
+
data = self.load(filename)
|
|
73
|
+
if self.expires is None or data["expires"] > time.time():
|
|
74
|
+
if data["key"] == key:
|
|
75
|
+
return data["value"]
|
|
76
|
+
|
|
77
|
+
value = proc()
|
|
78
|
+
data = {"key": key, "value": value}
|
|
79
|
+
if self.expires is not None:
|
|
80
|
+
data["expires"] = time.time() + self.expires
|
|
81
|
+
|
|
82
|
+
os.makedirs(path, exist_ok=True)
|
|
83
|
+
temp_filename = self.save(filename, data)
|
|
84
|
+
os.rename(temp_filename, filename)
|
|
85
|
+
|
|
86
|
+
CACHE[m] = value
|
|
87
|
+
return value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class JsonCacher(Cacher):
|
|
91
|
+
ext = ""
|
|
92
|
+
|
|
93
|
+
def save(self, path, data):
|
|
94
|
+
temp_path = path + ".tmp"
|
|
95
|
+
with open(temp_path, "w") as f:
|
|
96
|
+
json.dump(data, f)
|
|
97
|
+
return temp_path
|
|
98
|
+
|
|
99
|
+
def load(self, path):
|
|
100
|
+
with open(path, "r") as f:
|
|
101
|
+
return json.load(f)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class NpzCacher(Cacher):
|
|
105
|
+
ext = ".npz"
|
|
106
|
+
|
|
107
|
+
def save(self, path, data):
|
|
108
|
+
temp_path = path + ".tmp.npz"
|
|
109
|
+
np.savez(temp_path, **data)
|
|
110
|
+
return temp_path
|
|
111
|
+
|
|
112
|
+
def load(self, path):
|
|
113
|
+
return np.load(path, allow_pickle=True)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# PUBLIC API
|
|
117
|
+
def cached(collection="default", expires=None, encoding="json"):
|
|
118
|
+
"""Decorator to cache the result of a function.
|
|
119
|
+
|
|
120
|
+
Default is to use a json file to store the cache, but you can also use npz files
|
|
121
|
+
to cache dict of numpy arrays.
|
|
122
|
+
|
|
123
|
+
"""
|
|
124
|
+
return dict(json=JsonCacher, npz=NpzCacher)[encoding](collection, expires)
|
|
@@ -223,6 +223,23 @@ def load_any_dict_format(path) -> dict:
|
|
|
223
223
|
if path.endswith(".toml"):
|
|
224
224
|
with open(path, "rb") as f:
|
|
225
225
|
return tomllib.load(f)
|
|
226
|
+
|
|
227
|
+
if path == "-":
|
|
228
|
+
import sys
|
|
229
|
+
|
|
230
|
+
config = sys.stdin.read()
|
|
231
|
+
|
|
232
|
+
parsers = [(yaml.safe_load, "yaml"), (json.loads, "json"), (tomllib.loads, "toml")]
|
|
233
|
+
|
|
234
|
+
for parser, parser_type in parsers:
|
|
235
|
+
try:
|
|
236
|
+
LOG.debug(f"Trying {parser_type} parser for stdin")
|
|
237
|
+
return parser(config)
|
|
238
|
+
except Exception:
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
raise ValueError("Failed to parse configuration from stdin")
|
|
242
|
+
|
|
226
243
|
except (json.JSONDecodeError, yaml.YAMLError, tomllib.TOMLDecodeError) as e:
|
|
227
244
|
LOG.warning(f"Failed to parse config file {path}", exc_info=e)
|
|
228
245
|
raise ValueError(f"Failed to parse config file {path} [{e}]")
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# (C) Copyright 2024 Anemoi contributors.
|
|
2
|
+
#
|
|
3
|
+
# This software is licensed under the terms of the Apache Licence Version 2.0
|
|
4
|
+
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
|
|
5
|
+
#
|
|
6
|
+
# In applying this licence, ECMWF does not waive the privileges and immunities
|
|
7
|
+
# granted to it by virtue of its status as an intergovernmental organisation
|
|
8
|
+
# nor does it submit to any jurisdiction.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import cartopy.crs as ccrs
|
|
12
|
+
import cartopy.feature as cfeature
|
|
13
|
+
import matplotlib.pyplot as plt
|
|
14
|
+
import matplotlib.tri as tri
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
"""FOR DEVELOPMENT PURPOSES ONLY
|
|
18
|
+
|
|
19
|
+
This module contains
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# TODO: use earthkit-plots
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def fix(lons):
|
|
27
|
+
return np.where(lons > 180, lons - 360, lons)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def plot_values(
|
|
31
|
+
values, latitudes, longitudes, title=None, missing_value=None, min_value=None, max_value=None, **kwargs
|
|
32
|
+
):
|
|
33
|
+
|
|
34
|
+
_, ax = plt.subplots(subplot_kw={"projection": ccrs.PlateCarree()})
|
|
35
|
+
ax.coastlines()
|
|
36
|
+
ax.add_feature(cfeature.BORDERS, linestyle=":")
|
|
37
|
+
|
|
38
|
+
missing_values = np.isnan(values)
|
|
39
|
+
|
|
40
|
+
if missing_value is None:
|
|
41
|
+
values = values[~missing_values]
|
|
42
|
+
longitudes = longitudes[~missing_values]
|
|
43
|
+
latitudes = latitudes[~missing_values]
|
|
44
|
+
else:
|
|
45
|
+
values = np.where(missing_values, missing_value, values)
|
|
46
|
+
|
|
47
|
+
if max_value is not None:
|
|
48
|
+
values = np.where(values > max_value, max_value, values)
|
|
49
|
+
|
|
50
|
+
if min_value is not None:
|
|
51
|
+
values = np.where(values < min_value, min_value, values)
|
|
52
|
+
|
|
53
|
+
triangulation = tri.Triangulation(fix(longitudes), latitudes)
|
|
54
|
+
|
|
55
|
+
levels = kwargs.pop("levels", 10)
|
|
56
|
+
|
|
57
|
+
_ = ax.tricontourf(triangulation, values, levels=levels, transform=ccrs.PlateCarree())
|
|
58
|
+
|
|
59
|
+
options = dict(
|
|
60
|
+
levels=levels,
|
|
61
|
+
colors="black",
|
|
62
|
+
linewidths=0.5,
|
|
63
|
+
transform=ccrs.PlateCarree(),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
options.update(kwargs)
|
|
67
|
+
|
|
68
|
+
ax.tricontour(
|
|
69
|
+
triangulation,
|
|
70
|
+
values,
|
|
71
|
+
**options,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if title is not None:
|
|
75
|
+
ax.set_title(title)
|
|
76
|
+
|
|
77
|
+
return ax
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def plot_field(field, title=None, **kwargs):
|
|
81
|
+
values = field.to_numpy(flatten=True)
|
|
82
|
+
latitudes, longitudes = field.grid_points()
|
|
83
|
+
return plot_values(values, latitudes, longitudes, title=title, **kwargs)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# (C) Copyright 2025 Anemoi contributors.
|
|
2
|
+
#
|
|
3
|
+
# This software is licensed under the terms of the Apache Licence Version 2.0
|
|
4
|
+
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
|
|
5
|
+
#
|
|
6
|
+
# In applying this licence, ECMWF does not waive the privileges and immunities
|
|
7
|
+
# granted to it by virtue of its status as an intergovernmental organisation
|
|
8
|
+
# nor does it submit to any jurisdiction.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
"""Utilities for working with grids.
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
from io import BytesIO
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
import requests
|
|
21
|
+
|
|
22
|
+
from .caching import cached
|
|
23
|
+
|
|
24
|
+
LOG = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
GRIDS_URL_PATTERN = "https://get.ecmwf.int/repository/anemoi/grids/grid-{name}.npz"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def xyz_to_latlon(x, y, z):
|
|
31
|
+
return (
|
|
32
|
+
np.rad2deg(np.arcsin(np.minimum(1.0, np.maximum(-1.0, z)))),
|
|
33
|
+
np.rad2deg(np.arctan2(y, x)),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def latlon_to_xyz(lat, lon, radius=1.0):
|
|
38
|
+
# https://en.wikipedia.org/wiki/Geographic_coordinate_conversion#From_geodetic_to_ECEF_coordinates
|
|
39
|
+
# We assume that the Earth is a sphere of radius 1 so N(phi) = 1
|
|
40
|
+
# We assume h = 0
|
|
41
|
+
#
|
|
42
|
+
phi = np.deg2rad(lat)
|
|
43
|
+
lda = np.deg2rad(lon)
|
|
44
|
+
|
|
45
|
+
cos_phi = np.cos(phi)
|
|
46
|
+
cos_lda = np.cos(lda)
|
|
47
|
+
sin_phi = np.sin(phi)
|
|
48
|
+
sin_lda = np.sin(lda)
|
|
49
|
+
|
|
50
|
+
x = cos_phi * cos_lda * radius
|
|
51
|
+
y = cos_phi * sin_lda * radius
|
|
52
|
+
z = sin_phi * radius
|
|
53
|
+
|
|
54
|
+
return x, y, z
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def nearest_grid_points(source_latitudes, source_longitudes, target_latitudes, target_longitudes):
|
|
58
|
+
from scipy.spatial import cKDTree
|
|
59
|
+
|
|
60
|
+
source_xyz = latlon_to_xyz(source_latitudes, source_longitudes)
|
|
61
|
+
source_points = np.array(source_xyz).transpose()
|
|
62
|
+
|
|
63
|
+
target_xyz = latlon_to_xyz(target_latitudes, target_longitudes)
|
|
64
|
+
target_points = np.array(target_xyz).transpose()
|
|
65
|
+
|
|
66
|
+
_, indices = cKDTree(source_points).query(target_points, k=1)
|
|
67
|
+
return indices
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@cached(collection="grids", encoding="npz")
|
|
71
|
+
def _grids(name):
|
|
72
|
+
from anemoi.utils.config import load_config
|
|
73
|
+
|
|
74
|
+
user_path = load_config().get("utils", {}).get("grids_path")
|
|
75
|
+
if user_path:
|
|
76
|
+
path = os.path.expanduser(os.path.join(user_path, f"grid-{name}.npz"))
|
|
77
|
+
if os.path.exists(path):
|
|
78
|
+
LOG.warning("Loading grids from custom user path %s", path)
|
|
79
|
+
with open(path, "rb") as f:
|
|
80
|
+
return f.read()
|
|
81
|
+
else:
|
|
82
|
+
LOG.warning("Custom user path %s does not exist", path)
|
|
83
|
+
|
|
84
|
+
url = GRIDS_URL_PATTERN.format(name=name.lower())
|
|
85
|
+
LOG.warning("Downloading grids from %s", url)
|
|
86
|
+
response = requests.get(url)
|
|
87
|
+
response.raise_for_status()
|
|
88
|
+
return response.content
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def grids(name):
|
|
92
|
+
if name.endswith(".npz"):
|
|
93
|
+
return dict(np.load(name))
|
|
94
|
+
|
|
95
|
+
data = _grids(name)
|
|
96
|
+
npz = np.load(BytesIO(data))
|
|
97
|
+
return dict(npz)
|
|
@@ -689,3 +689,36 @@ def print_dates(dates) -> None:
|
|
|
689
689
|
A list of dates, as datetime objects or strings.
|
|
690
690
|
"""
|
|
691
691
|
print(compress_dates(dates))
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def make_list_int(value) -> list:
|
|
695
|
+
"""Convert a string like "1/2/3" or "1/to/3" or "1/to/10/by/2" to a list of integers.
|
|
696
|
+
|
|
697
|
+
Parameters
|
|
698
|
+
----------
|
|
699
|
+
value : str, list, tuple, int
|
|
700
|
+
The value to convert to a list of integers.
|
|
701
|
+
|
|
702
|
+
Returns
|
|
703
|
+
-------
|
|
704
|
+
list
|
|
705
|
+
A list of integers.
|
|
706
|
+
"""
|
|
707
|
+
if isinstance(value, str):
|
|
708
|
+
if "/" not in value:
|
|
709
|
+
return [int(value)]
|
|
710
|
+
bits = value.split("/")
|
|
711
|
+
if len(bits) == 3 and bits[1].lower() == "to":
|
|
712
|
+
value = list(range(int(bits[0]), int(bits[2]) + 1, 1))
|
|
713
|
+
|
|
714
|
+
elif len(bits) == 5 and bits[1].lower() == "to" and bits[3].lower() == "by":
|
|
715
|
+
value = list(range(int(bits[0]), int(bits[2]) + int(bits[4]), int(bits[4])))
|
|
716
|
+
|
|
717
|
+
if isinstance(value, list):
|
|
718
|
+
return value
|
|
719
|
+
if isinstance(value, tuple):
|
|
720
|
+
return value
|
|
721
|
+
if isinstance(value, int):
|
|
722
|
+
return [value]
|
|
723
|
+
|
|
724
|
+
raise ValueError(f"Cannot make list from {value}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: anemoi-utils
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.11
|
|
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
|
|
@@ -226,6 +226,7 @@ Requires-Python: >=3.9
|
|
|
226
226
|
License-File: LICENSE
|
|
227
227
|
Requires-Dist: aniso8601
|
|
228
228
|
Requires-Dist: importlib-metadata; python_version < "3.10"
|
|
229
|
+
Requires-Dist: numpy
|
|
229
230
|
Requires-Dist: python-dateutil
|
|
230
231
|
Requires-Dist: pyyaml
|
|
231
232
|
Requires-Dist: tomli; python_version < "3.11"
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
.gitignore
|
|
3
3
|
.pre-commit-config.yaml
|
|
4
4
|
.readthedocs.yaml
|
|
5
|
+
.release-please-config.json
|
|
6
|
+
.release-please-manifest.json
|
|
5
7
|
CHANGELOG.md
|
|
6
8
|
CONTRIBUTORS.md
|
|
7
9
|
LICENSE
|
|
@@ -9,13 +11,15 @@ README.md
|
|
|
9
11
|
pyproject.toml
|
|
10
12
|
.github/CODEOWNERS
|
|
11
13
|
.github/ci-hpc-config.yml
|
|
12
|
-
.github/
|
|
13
|
-
.github/workflows/changelog-release-update.yml
|
|
14
|
+
.github/release.yml
|
|
14
15
|
.github/workflows/ci.yml
|
|
15
16
|
.github/workflows/label-public-pr.yml
|
|
17
|
+
.github/workflows/merge-main-into-develop.yml
|
|
18
|
+
.github/workflows/pr-conventional-commit.yml
|
|
16
19
|
.github/workflows/python-publish.yml
|
|
17
20
|
.github/workflows/python-pull-request.yml
|
|
18
21
|
.github/workflows/readthedocs-pr-update.yml
|
|
22
|
+
.github/workflows/release-please.yml
|
|
19
23
|
docs/Makefile
|
|
20
24
|
docs/conf.py
|
|
21
25
|
docs/index.rst
|
|
@@ -40,7 +44,9 @@ src/anemoi/utils/cli.py
|
|
|
40
44
|
src/anemoi/utils/compatibility.py
|
|
41
45
|
src/anemoi/utils/config.py
|
|
42
46
|
src/anemoi/utils/dates.py
|
|
47
|
+
src/anemoi/utils/devtools.py
|
|
43
48
|
src/anemoi/utils/grib.py
|
|
49
|
+
src/anemoi/utils/grids.py
|
|
44
50
|
src/anemoi/utils/hindcasts.py
|
|
45
51
|
src/anemoi/utils/humanize.py
|
|
46
52
|
src/anemoi/utils/logs.py
|
|
@@ -66,9 +72,11 @@ src/anemoi_utils.egg-info/dependency_links.txt
|
|
|
66
72
|
src/anemoi_utils.egg-info/entry_points.txt
|
|
67
73
|
src/anemoi_utils.egg-info/requires.txt
|
|
68
74
|
src/anemoi_utils.egg-info/top_level.txt
|
|
75
|
+
tests/test_caching.py
|
|
69
76
|
tests/test_compatibility.py
|
|
70
77
|
tests/test_dates.py
|
|
71
78
|
tests/test_frequency.py
|
|
79
|
+
tests/test_grids.py
|
|
72
80
|
tests/test_provenance.py
|
|
73
81
|
tests/test_remote.py
|
|
74
82
|
tests/test_sanetise.py
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# (C) Copyright 2025 Anemoi contributors.
|
|
2
|
+
#
|
|
3
|
+
# This software is licensed under the terms of the Apache Licence Version 2.0
|
|
4
|
+
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
|
|
5
|
+
#
|
|
6
|
+
# In applying this licence, ECMWF does not waive the privileges and immunities
|
|
7
|
+
# granted to it by virtue of its status as an intergovernmental organisation
|
|
8
|
+
# nor does it submit to any jurisdiction.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from anemoi.utils.caching import cached
|
|
14
|
+
from anemoi.utils.caching import clean_cache
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def check(f, data):
|
|
18
|
+
"""Check that the function f returns the expected values from the data.
|
|
19
|
+
The function f is called three times for each value in the data.
|
|
20
|
+
The number of actual calls to the function is checked to make sure the cache is used when it should be.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
for i, x in enumerate(data):
|
|
24
|
+
assert data.n == i
|
|
25
|
+
|
|
26
|
+
res = f(x)
|
|
27
|
+
assert type(res) == type(data[x]) # noqa: E721
|
|
28
|
+
assert str(res) == str(data[x])
|
|
29
|
+
assert data.n == i + 1
|
|
30
|
+
|
|
31
|
+
res = f(x)
|
|
32
|
+
assert type(res) == type(data[x]) # noqa: E721
|
|
33
|
+
assert str(res) == str(data[x])
|
|
34
|
+
assert data.n == i + 1
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Data(dict):
|
|
38
|
+
"""Simple class to store data and count the number of calls to the function."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, *args, **kwargs):
|
|
41
|
+
super().__init__(*args, **kwargs)
|
|
42
|
+
self.n = 0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
global DATA
|
|
46
|
+
|
|
47
|
+
#########################################
|
|
48
|
+
# basic test
|
|
49
|
+
#########################################
|
|
50
|
+
global values_a
|
|
51
|
+
values_a = Data(a=1, b=2)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@cached(collection="test", expires=0)
|
|
55
|
+
def func_a(x):
|
|
56
|
+
global values_a
|
|
57
|
+
values_a.n += 1
|
|
58
|
+
return values_a[x]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_cached_basic(*values, **kwargs):
|
|
62
|
+
clean_cache("test")
|
|
63
|
+
check(func_a, values_a)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
#########################################
|
|
67
|
+
# Test with numpy arrays
|
|
68
|
+
#########################################
|
|
69
|
+
|
|
70
|
+
global values_c
|
|
71
|
+
values_c = Data(
|
|
72
|
+
a=dict(A=np.array([1, 2, 3]), B=np.array([4, 5, 6])),
|
|
73
|
+
b=dict(A=np.array([7, 8, 9]), B=np.array([10, 11, 12])),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@cached(collection="test", expires=0, encoding="npz")
|
|
78
|
+
def func_c(x):
|
|
79
|
+
global values_c
|
|
80
|
+
values_c.n += 1
|
|
81
|
+
return values_c[x]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_cached_npz(*values, **kwargs):
|
|
85
|
+
clean_cache("test")
|
|
86
|
+
check(func_c, values_c)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
#########################################
|
|
90
|
+
# Test with a various types
|
|
91
|
+
global values_d
|
|
92
|
+
values_d = Data(a="4", b=5.0, c=dict(d=6), e=[7, 8, 9], f=(10, 11, 12))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@cached(collection="test", expires=0)
|
|
96
|
+
def func_d(x):
|
|
97
|
+
global values_d
|
|
98
|
+
values_d.n += 1
|
|
99
|
+
return values_d[x]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_cached_various_types(*values, **kwargs):
|
|
103
|
+
clean_cache("test")
|
|
104
|
+
check(func_d, values_d)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
#########################################
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
for name, obj in list(globals().items()):
|
|
111
|
+
if name.startswith("test_") and callable(obj):
|
|
112
|
+
print(f"Running {name}...")
|
|
113
|
+
obj()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# (C) Copyright 2025 Anemoi contributors.
|
|
2
|
+
#
|
|
3
|
+
# This software is licensed under the terms of the Apache Licence Version 2.0
|
|
4
|
+
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
|
|
5
|
+
#
|
|
6
|
+
# In applying this licence, ECMWF does not waive the privileges and immunities
|
|
7
|
+
# granted to it by virtue of its status as an intergovernmental organisation
|
|
8
|
+
# nor does it submit to any jurisdiction.
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from anemoi.utils.grids import grids
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_o96():
|
|
15
|
+
x = grids("o96")
|
|
16
|
+
assert x["latitudes"].mean() == 0.0
|
|
17
|
+
assert x["longitudes"].mean() == 179.14285714285714
|
|
18
|
+
assert x["latitudes"].shape == (40320,)
|
|
19
|
+
assert x["longitudes"].shape == (40320,)
|
|
20
|
+
assert x["latitudes"][31415] == -31.324557701757268
|
|
21
|
+
assert x["longitudes"][31415] == 224.32835820895522
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
for name, obj in list(globals().items()):
|
|
26
|
+
if name.startswith("test_") and callable(obj):
|
|
27
|
+
print(f"Running {name}...")
|
|
28
|
+
obj()
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# name: Check Changelog Update on PR
|
|
2
|
-
# on:
|
|
3
|
-
# pull_request:
|
|
4
|
-
# types: [assigned, opened, synchronize, reopened, labeled, unlabeled]
|
|
5
|
-
# branches:
|
|
6
|
-
# - main
|
|
7
|
-
# - develop
|
|
8
|
-
# paths-ignore:
|
|
9
|
-
# - .pre-commit-config.yaml
|
|
10
|
-
# - .readthedocs.yaml
|
|
11
|
-
# jobs:
|
|
12
|
-
# Check-Changelog:
|
|
13
|
-
# name: Check Changelog Action
|
|
14
|
-
# runs-on: ubuntu-20.04
|
|
15
|
-
# steps:
|
|
16
|
-
# - uses: tarides/changelog-check-action@v2
|
|
17
|
-
# with:
|
|
18
|
-
# changelog: CHANGELOG.md
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# .github/workflows/update-changelog.yaml
|
|
2
|
-
name: "Update Changelog"
|
|
3
|
-
|
|
4
|
-
on:
|
|
5
|
-
workflow_run:
|
|
6
|
-
workflows:
|
|
7
|
-
- Upload Python Package
|
|
8
|
-
types:
|
|
9
|
-
- completed
|
|
10
|
-
|
|
11
|
-
permissions:
|
|
12
|
-
pull-requests: write
|
|
13
|
-
contents: write
|
|
14
|
-
|
|
15
|
-
jobs:
|
|
16
|
-
update:
|
|
17
|
-
runs-on: ubuntu-latest
|
|
18
|
-
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
|
19
|
-
|
|
20
|
-
steps:
|
|
21
|
-
- name: Checkout code
|
|
22
|
-
uses: actions/checkout@v4
|
|
23
|
-
with:
|
|
24
|
-
ref: ${{ github.event.release.target_commitish }}
|
|
25
|
-
|
|
26
|
-
- name: Update Changelog
|
|
27
|
-
uses: stefanzweifel/changelog-updater-action@v1
|
|
28
|
-
with:
|
|
29
|
-
latest-version: ${{ github.event.release.tag_name }}
|
|
30
|
-
heading-text: ${{ github.event.release.name }}
|
|
31
|
-
release-notes: ${{ github.event.release.body }}
|
|
32
|
-
|
|
33
|
-
- name: Create Pull Request
|
|
34
|
-
uses: peter-evans/create-pull-request@v6
|
|
35
|
-
with:
|
|
36
|
-
branch: docs/changelog-update-${{ github.event.release.tag_name }}
|
|
37
|
-
base: develop
|
|
38
|
-
title: '[Changelog] Update to ${{ github.event.release.tag_name }}'
|
|
39
|
-
body: |
|
|
40
|
-
This PR updates the changelog to include the changes in the latest release.
|
|
41
|
-
|
|
42
|
-
> [!CAUTION]
|
|
43
|
-
> Merge DO NOT squash to correctly update the tag version of `develop` branch.
|
|
44
|
-
add-paths: |
|
|
45
|
-
CHANGELOG.md
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# (C) Copyright 2024 Anemoi contributors.
|
|
2
|
-
#
|
|
3
|
-
# This software is licensed under the terms of the Apache Licence Version 2.0
|
|
4
|
-
# which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
|
|
5
|
-
#
|
|
6
|
-
# In applying this licence, ECMWF does not waive the privileges and immunities
|
|
7
|
-
# granted to it by virtue of its status as an intergovernmental organisation
|
|
8
|
-
# nor does it submit to any jurisdiction.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import hashlib
|
|
12
|
-
import json
|
|
13
|
-
import os
|
|
14
|
-
import time
|
|
15
|
-
from threading import Lock
|
|
16
|
-
|
|
17
|
-
LOCK = Lock()
|
|
18
|
-
CACHE = {}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def cache(key, proc, collection="default", expires=None):
|
|
22
|
-
|
|
23
|
-
key = json.dumps(key, sort_keys=True)
|
|
24
|
-
m = hashlib.md5()
|
|
25
|
-
m.update(key.encode("utf-8"))
|
|
26
|
-
m = m.hexdigest()
|
|
27
|
-
|
|
28
|
-
if m in CACHE:
|
|
29
|
-
return CACHE[m]
|
|
30
|
-
|
|
31
|
-
path = os.path.join(os.path.expanduser("~"), ".cache", "anemoi", collection)
|
|
32
|
-
os.makedirs(path, exist_ok=True)
|
|
33
|
-
|
|
34
|
-
filename = os.path.join(path, m)
|
|
35
|
-
if os.path.exists(filename):
|
|
36
|
-
with open(filename, "r") as f:
|
|
37
|
-
data = json.load(f)
|
|
38
|
-
if expires is None or data["expires"] > time.time():
|
|
39
|
-
if data["key"] == key:
|
|
40
|
-
return data["value"]
|
|
41
|
-
|
|
42
|
-
value = proc()
|
|
43
|
-
data = {"key": key, "value": value}
|
|
44
|
-
if expires is not None:
|
|
45
|
-
data["expires"] = time.time() + expires
|
|
46
|
-
|
|
47
|
-
with open(filename, "w") as f:
|
|
48
|
-
json.dump(data, f)
|
|
49
|
-
|
|
50
|
-
CACHE[m] = value
|
|
51
|
-
return value
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class cached:
|
|
55
|
-
"""Decorator to cache the result of a function."""
|
|
56
|
-
|
|
57
|
-
def __init__(self, collection="default", expires=None):
|
|
58
|
-
self.collection = collection
|
|
59
|
-
self.expires = expires
|
|
60
|
-
|
|
61
|
-
def __call__(self, func):
|
|
62
|
-
|
|
63
|
-
full = f"{func.__module__}.{func.__name__}"
|
|
64
|
-
|
|
65
|
-
def wrapped(*args, **kwargs):
|
|
66
|
-
with LOCK:
|
|
67
|
-
return cache(
|
|
68
|
-
(full, args, kwargs),
|
|
69
|
-
lambda: func(*args, **kwargs),
|
|
70
|
-
self.collection,
|
|
71
|
-
self.expires,
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
return wrapped
|
|
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
|