anemoi-utils 0.4.35__tar.gz → 0.4.37__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of anemoi-utils might be problematic. Click here for more details.

Files changed (114) hide show
  1. anemoi_utils-0.4.37/.github/workflows/pr-label-ats.yml +66 -0
  2. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.pre-commit-config.yaml +2 -2
  3. anemoi_utils-0.4.37/.release-please-manifest.json +3 -0
  4. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/CHANGELOG.md +30 -0
  5. {anemoi_utils-0.4.35/src/anemoi_utils.egg-info → anemoi_utils-0.4.37}/PKG-INFO +3 -2
  6. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/pyproject.toml +2 -1
  7. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/_environment.py +3 -0
  8. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/_version.py +3 -3
  9. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/checkpoints.py +47 -32
  10. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/cli.py +38 -5
  11. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/commands/metadata.py +6 -4
  12. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/commands/transfer.py +6 -2
  13. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/config.py +0 -6
  14. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/logs.py +34 -6
  15. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/mlflow/auth.py +154 -18
  16. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/registry.py +55 -1
  17. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/remote/__init__.py +1 -3
  18. anemoi_utils-0.4.37/src/anemoi/utils/remote/s3.py +735 -0
  19. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37/src/anemoi_utils.egg-info}/PKG-INFO +3 -2
  20. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi_utils.egg-info/SOURCES.txt +4 -0
  21. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi_utils.egg-info/requires.txt +2 -1
  22. anemoi_utils-0.4.37/tests/test_checkpoints.py +220 -0
  23. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/tests/test_mlflow_auth.py +130 -4
  24. anemoi_utils-0.4.37/tests/test_registry.py +64 -0
  25. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/tests/test_remote.py +45 -23
  26. anemoi_utils-0.4.37/tests/test_s3.py +65 -0
  27. anemoi_utils-0.4.35/.release-please-manifest.json +0 -3
  28. anemoi_utils-0.4.35/src/anemoi/utils/remote/s3.py +0 -789
  29. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.gitattributes +0 -0
  30. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.github/CODEOWNERS +0 -0
  31. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.github/ci-hpc-config.yml +0 -0
  32. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.github/dependabot.yml +0 -0
  33. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.github/labeler.yml +0 -0
  34. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.github/pull_request_template.md +0 -0
  35. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.github/workflows/downstream-ci-hpc.yml +0 -0
  36. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.github/workflows/pr-conventional-commit.yml +0 -0
  37. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.github/workflows/pr-label-conventional-commits.yml +0 -0
  38. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.github/workflows/pr-label-file-based.yml +0 -0
  39. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.github/workflows/pr-label-public.yml +0 -0
  40. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.github/workflows/python-publish.yml +0 -0
  41. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.github/workflows/python-pull-request.yml +0 -0
  42. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.github/workflows/readthedocs-pr-update.yml +0 -0
  43. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.github/workflows/release-please.yml +0 -0
  44. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.gitignore +0 -0
  45. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.readthedocs.yaml +0 -0
  46. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/.release-please-config.json +0 -0
  47. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/CONTRIBUTORS.md +0 -0
  48. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/LICENSE +0 -0
  49. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/README.md +0 -0
  50. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/docs/Makefile +0 -0
  51. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/docs/_static/logo.png +0 -0
  52. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/docs/_static/style.css +0 -0
  53. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/docs/_templates/.gitkeep +0 -0
  54. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/docs/_templates/apidoc/package.rst.jinja +0 -0
  55. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/docs/conf.py +0 -0
  56. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/docs/index.rst +0 -0
  57. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/docs/installing.rst +0 -0
  58. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/docs/modules/checkpoints.rst +0 -0
  59. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/docs/modules/config.rst +0 -0
  60. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/docs/modules/dates.rst +0 -0
  61. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/docs/modules/grib.rst +0 -0
  62. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/docs/modules/humanize.rst +0 -0
  63. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/docs/modules/provenance.rst +0 -0
  64. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/docs/modules/s3.rst +0 -0
  65. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/docs/modules/testing.rst +0 -0
  66. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/docs/modules/text.rst +0 -0
  67. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/docs/scripts/api_build.sh +0 -0
  68. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/setup.cfg +0 -0
  69. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/__init__.py +0 -0
  70. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/__main__.py +0 -0
  71. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/caching.py +0 -0
  72. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/commands/__init__.py +0 -0
  73. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/commands/config.py +0 -0
  74. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/commands/requests.py +0 -0
  75. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/compatibility.py +0 -0
  76. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/dates.py +0 -0
  77. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/devtools.py +0 -0
  78. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/grib.py +0 -0
  79. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/grids.py +0 -0
  80. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/hindcasts.py +0 -0
  81. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/humanize.py +0 -0
  82. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/mars/__init__.py +0 -0
  83. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/mars/mars.yaml +0 -0
  84. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/mars/requests.py +0 -0
  85. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/mlflow/__init__.py +0 -0
  86. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/mlflow/client.py +0 -0
  87. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/mlflow/utils.py +0 -0
  88. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/provenance.py +0 -0
  89. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/remote/ssh.py +0 -0
  90. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/rules.py +0 -0
  91. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/s3.py +0 -0
  92. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/sanitise.py +0 -0
  93. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/sanitize.py +0 -0
  94. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/schemas/__init__.py +0 -0
  95. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/schemas/errors.py +0 -0
  96. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/testing.py +0 -0
  97. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/text.py +0 -0
  98. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi/utils/timer.py +0 -0
  99. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi_utils.egg-info/dependency_links.txt +0 -0
  100. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi_utils.egg-info/entry_points.txt +0 -0
  101. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/src/anemoi_utils.egg-info/top_level.txt +0 -0
  102. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/tests/test-transfer-data/directory/b/c/x +0 -0
  103. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/tests/test-transfer-data/directory/b/y +0 -0
  104. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/tests/test-transfer-data/directory/exotic filename ;^/"'[=.,#]()/303/252/303/274/303/247/303/262/342/234/205.txt" +0 -0
  105. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/tests/test-transfer-data/directory/z +0 -0
  106. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/tests/test-transfer-data/file +0 -0
  107. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/tests/test_caching.py +0 -0
  108. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/tests/test_compatibility.py +0 -0
  109. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/tests/test_dates.py +0 -0
  110. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/tests/test_frequency.py +0 -0
  111. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/tests/test_mlflow_client.py +0 -0
  112. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/tests/test_provenance.py +0 -0
  113. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/tests/test_sanitise.py +0 -0
  114. {anemoi_utils-0.4.35 → anemoi_utils-0.4.37}/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: v5.0.0
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.7
44
+ rev: v0.12.11
45
45
  hooks:
46
46
  - id: ruff
47
47
  args:
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.4.37"
3
+ }
@@ -8,6 +8,36 @@ 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.37](https://github.com/ecmwf/anemoi-utils/compare/0.4.36...0.4.37) (2025-09-30)
12
+
13
+
14
+ ### Features
15
+
16
+ * **mlflow auth:** Support for multiple servers ([#217](https://github.com/ecmwf/anemoi-utils/issues/217)) ([8ccfb1a](https://github.com/ecmwf/anemoi-utils/commit/8ccfb1ab063cccfec5852c386580036286b097c6))
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * Update s3 chunk size to 10 MB ([#220](https://github.com/ecmwf/anemoi-utils/issues/220)) ([aa20fa8](https://github.com/ecmwf/anemoi-utils/commit/aa20fa8b0b572fb6fa510b2f28c2b8b8a2f76d7c))
22
+ * Use `yaml` and `json` flag in metadata get command ([#222](https://github.com/ecmwf/anemoi-utils/issues/222)) ([6af46c4](https://github.com/ecmwf/anemoi-utils/commit/6af46c4e715fc55aca374d2112976aa7d1bac589))
23
+
24
+ ## [0.4.36](https://github.com/ecmwf/anemoi-utils/compare/0.4.35...0.4.36) (2025-09-22)
25
+
26
+
27
+ ### Features
28
+
29
+ * Add aliases to registry ([#219](https://github.com/ecmwf/anemoi-utils/issues/219)) ([37267b5](https://github.com/ecmwf/anemoi-utils/commit/37267b548556a796a01b43abb908011eeec85454))
30
+ * Debug imports ([#182](https://github.com/ecmwf/anemoi-utils/issues/182)) ([1eaa615](https://github.com/ecmwf/anemoi-utils/commit/1eaa61540dc9ac3d5fe82f2c91b7fc98c8bb10af))
31
+ * NoAuth for AML mlflow Logging ([#200](https://github.com/ecmwf/anemoi-utils/issues/200)) ([732182e](https://github.com/ecmwf/anemoi-utils/commit/732182ea5d255ba69ea2ed0a23b307d6f64aaf84))
32
+ * Rich logging ([#209](https://github.com/ecmwf/anemoi-utils/issues/209)) ([3c762a5](https://github.com/ecmwf/anemoi-utils/commit/3c762a593ba2dc734becc54b92984d6dc62967ac))
33
+ * Speedup checkpoint editing - remove compression ([#218](https://github.com/ecmwf/anemoi-utils/issues/218)) ([b49120f](https://github.com/ecmwf/anemoi-utils/commit/b49120f763b0b6ee10c805bab2aa7b973047f963))
34
+ * Use obstore to access s3 buckets ([#210](https://github.com/ecmwf/anemoi-utils/issues/210)) ([da380be](https://github.com/ecmwf/anemoi-utils/commit/da380be71d78274d72bd0a3859ef00b1c80e9469))
35
+
36
+
37
+ ### Bug Fixes
38
+
39
+ * 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))
40
+
11
41
  ## [0.4.35](https://github.com/ecmwf/anemoi-utils/compare/0.4.34...0.4.35) (2025-08-12)
12
42
 
13
43
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anemoi-utils
3
- Version: 0.4.35
3
+ Version: 0.4.37
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: boto3>1.36; extra == "s3"
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
- "boto3>1.36",
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.35'
32
- __version_tuple__ = version_tuple = (0, 4, 35)
31
+ __version__ = version = '0.4.37'
32
+ __version_tuple__ = version_tuple = (0, 4, 37)
33
33
 
34
- __commit_id__ = commit_id = 'gd34987af9'
34
+ __commit_id__ = commit_id = 'gceecf1ec8'
@@ -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
- found = False
234
-
235
- directory = None
236
- with TemporaryDirectory() as temp_dir:
237
- zipfile.ZipFile(path, "r").extractall(temp_dir)
238
- total = 0
239
- for root, dirs, files in os.walk(temp_dir):
240
- for f in files:
241
- total += 1
242
- full = os.path.join(root, f)
243
- if f == name:
244
- found = True
245
- callback(full)
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
- for key, entry in supporting_arrays.items():
254
- value = entry.tobytes()
255
- fname = os.path.join(directory, f"{key}.numpy")
256
- os.makedirs(os.path.dirname(fname), exist_ok=True)
257
- with open(fname, "wb") as f:
258
- f.write(value)
259
- total += 1
260
-
261
- with zipfile.ZipFile(new_path, "w", zipfile.ZIP_DEFLATED) as zipf:
262
- with tqdm.tqdm(total=total, desc="Rebuilding checkpoint") as pbar:
263
- for root, dirs, files in os.walk(temp_dir):
264
- for f in files:
265
- full = os.path.join(root, f)
266
- rel = os.path.relpath(full, temp_dir)
267
- zipf.write(full, rel)
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
- logging.basicConfig(
220
- format="%(asctime)s %(levelname)s %(message)s",
221
- datefmt="%Y-%m-%d %H:%M:%S",
222
- level=logging.DEBUG if args.debug else logging.INFO,
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)
@@ -139,13 +139,13 @@ class Metadata(Command):
139
139
  command_parser.add_argument(
140
140
  "--json",
141
141
  action="store_true",
142
- help="Use the JSON format with ``--dump``, ``--view`` and ``--edit``.",
142
+ help="Use the JSON format with ``--dump``, ``--view``, ``--get`` and ``--edit``.",
143
143
  )
144
144
 
145
145
  command_parser.add_argument(
146
146
  "--yaml",
147
147
  action="store_true",
148
- help="Use the YAML format with ``--dump``, ``--view`` and ``--edit``.",
148
+ help="Use the YAML format with ``--dump``, ``--view``, ``--get`` and ``--edit``.",
149
149
  )
150
150
 
151
151
  def run(self, args: Namespace) -> None:
@@ -315,7 +315,6 @@ class Metadata(Command):
315
315
  args : Namespace
316
316
  The arguments passed to the command.
317
317
  """
318
- from pprint import pprint
319
318
 
320
319
  from anemoi.utils.checkpoints import load_metadata
321
320
 
@@ -335,7 +334,10 @@ class Metadata(Command):
335
334
 
336
335
  print(f"Metadata values for {args.get}: ", end="\n" if isinstance(metadata, (dict, list)) else "")
337
336
  if isinstance(metadata, dict):
338
- pprint(metadata, indent=2, compact=True)
337
+ if args.yaml:
338
+ print(yaml.dump(metadata, indent=2, sort_keys=True))
339
+ return
340
+ print(json.dumps(metadata, indent=2, sort_keys=True))
339
341
  else:
340
342
  print(metadata)
341
343
 
@@ -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", help="A path to a local file or folder or a URL to a file or a folder on S3."
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", help="A path to a local file or folder or a URL to a file or a folder on S3 or a remote folder."
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
 
@@ -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
- thread_local = threading.local()
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
- thread_local.logging_name = name
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 = thread_local.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
- formatter = ThreadCustomFormatter("%(asctime)s - %(logging_name)s - %(levelname)s - %(message)s")
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]")