anemoi-utils 0.4.36__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.
- anemoi_utils-0.4.37/.release-please-manifest.json +3 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/CHANGELOG.md +13 -0
- {anemoi_utils-0.4.36/src/anemoi_utils.egg-info → anemoi_utils-0.4.37}/PKG-INFO +1 -1
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/_version.py +3 -3
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/commands/metadata.py +6 -4
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/mlflow/auth.py +115 -17
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/remote/s3.py +2 -2
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37/src/anemoi_utils.egg-info}/PKG-INFO +1 -1
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/tests/test_mlflow_auth.py +113 -4
- anemoi_utils-0.4.36/.release-please-manifest.json +0 -3
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.gitattributes +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.github/CODEOWNERS +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.github/ci-hpc-config.yml +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.github/dependabot.yml +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.github/labeler.yml +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.github/pull_request_template.md +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.github/workflows/downstream-ci-hpc.yml +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.github/workflows/pr-conventional-commit.yml +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.github/workflows/pr-label-ats.yml +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.github/workflows/pr-label-conventional-commits.yml +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.github/workflows/pr-label-file-based.yml +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.github/workflows/pr-label-public.yml +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.github/workflows/python-publish.yml +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.github/workflows/python-pull-request.yml +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.github/workflows/readthedocs-pr-update.yml +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.github/workflows/release-please.yml +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.gitignore +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.pre-commit-config.yaml +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.readthedocs.yaml +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.release-please-config.json +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/CONTRIBUTORS.md +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/LICENSE +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/README.md +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/docs/Makefile +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/docs/_static/logo.png +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/docs/_static/style.css +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/docs/_templates/.gitkeep +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/docs/_templates/apidoc/package.rst.jinja +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/docs/conf.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/docs/index.rst +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/docs/installing.rst +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/docs/modules/checkpoints.rst +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/docs/modules/config.rst +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/docs/modules/dates.rst +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/docs/modules/grib.rst +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/docs/modules/humanize.rst +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/docs/modules/provenance.rst +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/docs/modules/s3.rst +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/docs/modules/testing.rst +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/docs/modules/text.rst +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/docs/scripts/api_build.sh +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/pyproject.toml +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/setup.cfg +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/__init__.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/__main__.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/_environment.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/caching.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/checkpoints.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/cli.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/commands/__init__.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/commands/config.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/commands/requests.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/commands/transfer.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/compatibility.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/config.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/dates.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/devtools.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/grib.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/grids.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/hindcasts.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/humanize.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/logs.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/mars/__init__.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/mars/mars.yaml +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/mars/requests.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/mlflow/__init__.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/mlflow/client.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/mlflow/utils.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/provenance.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/registry.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/remote/__init__.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/remote/ssh.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/rules.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/s3.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/sanitise.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/sanitize.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/schemas/__init__.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/schemas/errors.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/testing.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/text.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi/utils/timer.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi_utils.egg-info/SOURCES.txt +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi_utils.egg-info/dependency_links.txt +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi_utils.egg-info/entry_points.txt +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi_utils.egg-info/requires.txt +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/src/anemoi_utils.egg-info/top_level.txt +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/tests/test-transfer-data/directory/b/c/x +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/tests/test-transfer-data/directory/b/y +0 -0
- {anemoi_utils-0.4.36 → 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
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/tests/test-transfer-data/directory/z +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/tests/test-transfer-data/file +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/tests/test_caching.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/tests/test_checkpoints.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/tests/test_compatibility.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/tests/test_dates.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/tests/test_frequency.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/tests/test_mlflow_client.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/tests/test_provenance.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/tests/test_registry.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/tests/test_remote.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/tests/test_s3.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/tests/test_sanitise.py +0 -0
- {anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/tests/test_utils.py +0 -0
|
@@ -8,6 +8,19 @@ 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
|
+
|
|
11
24
|
## [0.4.36](https://github.com/ecmwf/anemoi-utils/compare/0.4.35...0.4.36) (2025-09-22)
|
|
12
25
|
|
|
13
26
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anemoi-utils
|
|
3
|
-
Version: 0.4.
|
|
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
|
|
@@ -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.37'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 4, 37)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'gceecf1ec8'
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
import logging
|
|
14
14
|
import os
|
|
15
15
|
import time
|
|
16
|
+
import warnings
|
|
16
17
|
from abc import ABC
|
|
17
18
|
from abc import abstractmethod
|
|
18
19
|
from datetime import datetime
|
|
@@ -22,10 +23,15 @@ from getpass import getpass
|
|
|
22
23
|
from typing import TYPE_CHECKING
|
|
23
24
|
|
|
24
25
|
import requests
|
|
26
|
+
from pydantic import BaseModel
|
|
27
|
+
from pydantic import RootModel
|
|
28
|
+
from pydantic import field_validator
|
|
29
|
+
from pydantic import model_validator
|
|
25
30
|
from requests.exceptions import HTTPError
|
|
26
31
|
|
|
32
|
+
from ..config import CONFIG_LOCK
|
|
27
33
|
from ..config import config_path
|
|
28
|
-
from ..config import
|
|
34
|
+
from ..config import load_raw_config
|
|
29
35
|
from ..config import save_config
|
|
30
36
|
from ..remote import robust
|
|
31
37
|
from ..timer import Timer
|
|
@@ -37,6 +43,56 @@ if TYPE_CHECKING:
|
|
|
37
43
|
from collections.abc import Callable
|
|
38
44
|
|
|
39
45
|
|
|
46
|
+
class ServerConfig(BaseModel):
|
|
47
|
+
refresh_token: str | None = None
|
|
48
|
+
refresh_expires: int = 0
|
|
49
|
+
|
|
50
|
+
@field_validator("refresh_expires", mode="before")
|
|
51
|
+
def to_int(cls, value: float | int) -> int:
|
|
52
|
+
if not isinstance(value, int):
|
|
53
|
+
return int(value)
|
|
54
|
+
return value
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ServerStore(RootModel):
|
|
58
|
+
root: dict[str, ServerConfig] = {}
|
|
59
|
+
|
|
60
|
+
def get(self, url: str) -> ServerConfig | None:
|
|
61
|
+
return self.root.get(url)
|
|
62
|
+
|
|
63
|
+
def __getitem__(self, url: str) -> ServerConfig:
|
|
64
|
+
return self.root[url]
|
|
65
|
+
|
|
66
|
+
def items(self):
|
|
67
|
+
return self.root.items()
|
|
68
|
+
|
|
69
|
+
def update(self, url, config: ServerConfig) -> None:
|
|
70
|
+
"""Update the server configuration for a given URL."""
|
|
71
|
+
self.root[url] = config
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def servers(self) -> list[tuple[str, int]]:
|
|
75
|
+
"""List of servers in the store, as a tuple (url, refresh_expires). Ordered most recently used first."""
|
|
76
|
+
return [
|
|
77
|
+
(url, cfg.refresh_expires)
|
|
78
|
+
for url, cfg in sorted(
|
|
79
|
+
self.root.items(),
|
|
80
|
+
key=lambda item: item[1].refresh_expires,
|
|
81
|
+
reverse=True,
|
|
82
|
+
)
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
@model_validator(mode="before")
|
|
86
|
+
@classmethod
|
|
87
|
+
def load_legacy_format(cls, data: dict) -> dict:
|
|
88
|
+
"""Convert legacy single-server config format to multi-server."""
|
|
89
|
+
if isinstance(data, dict) and "url" in data:
|
|
90
|
+
_data = data.copy()
|
|
91
|
+
_url = _data.pop("url")
|
|
92
|
+
data = {_url: ServerConfig(**_data)}
|
|
93
|
+
return data
|
|
94
|
+
|
|
95
|
+
|
|
40
96
|
class AuthBase(ABC):
|
|
41
97
|
"""Base class for authentication implementations."""
|
|
42
98
|
|
|
@@ -76,7 +132,7 @@ class NoAuth(AuthBase):
|
|
|
76
132
|
class TokenAuth(AuthBase):
|
|
77
133
|
"""Manage authentication with a keycloak token server."""
|
|
78
134
|
|
|
79
|
-
|
|
135
|
+
_config_file = "mlflow-token.json"
|
|
80
136
|
|
|
81
137
|
def __init__(
|
|
82
138
|
self,
|
|
@@ -101,10 +157,16 @@ class TokenAuth(AuthBase):
|
|
|
101
157
|
self.target_env_var = target_env_var
|
|
102
158
|
self._enabled = enabled
|
|
103
159
|
|
|
104
|
-
|
|
160
|
+
store = self._get_store()
|
|
161
|
+
config = store.get(self.url)
|
|
162
|
+
|
|
163
|
+
if config is not None:
|
|
164
|
+
self._refresh_token = config.refresh_token
|
|
165
|
+
self.refresh_expires = config.refresh_expires
|
|
166
|
+
else:
|
|
167
|
+
self._refresh_token = None
|
|
168
|
+
self.refresh_expires = 0
|
|
105
169
|
|
|
106
|
-
self._refresh_token = config.get("refresh_token")
|
|
107
|
-
self.refresh_expires = config.get("refresh_expires", 0)
|
|
108
170
|
self.access_token = None
|
|
109
171
|
self.access_expires = 0
|
|
110
172
|
|
|
@@ -124,17 +186,50 @@ class TokenAuth(AuthBase):
|
|
|
124
186
|
self._refresh_token = value
|
|
125
187
|
self.refresh_expires = time.time() + (REFRESH_EXPIRE_DAYS * 86400) # 86400 seconds in a day
|
|
126
188
|
|
|
189
|
+
@staticmethod
|
|
190
|
+
def _get_store() -> ServerStore:
|
|
191
|
+
"""Read the server store from disk."""
|
|
192
|
+
with CONFIG_LOCK:
|
|
193
|
+
file = TokenAuth._config_file
|
|
194
|
+
path = config_path(file)
|
|
195
|
+
|
|
196
|
+
if not os.path.exists(path):
|
|
197
|
+
save_config(file, {})
|
|
198
|
+
|
|
199
|
+
if os.path.exists(path) and os.stat(path).st_mode & 0o777 != 0o600:
|
|
200
|
+
os.chmod(path, 0o600)
|
|
201
|
+
|
|
202
|
+
return ServerStore(**load_raw_config(file))
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def get_servers() -> list[tuple[str, int]]:
|
|
206
|
+
"""List of all saved servers, as a tuple (url, refresh_expires). Ordered most recently used first."""
|
|
207
|
+
return TokenAuth._get_store().servers
|
|
208
|
+
|
|
127
209
|
@staticmethod
|
|
128
210
|
def load_config() -> dict:
|
|
129
|
-
|
|
211
|
+
"""Load the last used server configuration
|
|
130
212
|
|
|
131
|
-
|
|
132
|
-
|
|
213
|
+
Returns
|
|
214
|
+
-------
|
|
215
|
+
config : dict
|
|
216
|
+
Dictionary with the following keys: `url`, `refresh_token`, `refresh_expires`.
|
|
217
|
+
If no configuration is found, an empty dictionary is returned.
|
|
218
|
+
"""
|
|
219
|
+
warnings.warn(
|
|
220
|
+
"TokenAuth.load_config() is deprecated and will be removed in a future release.",
|
|
221
|
+
DeprecationWarning,
|
|
222
|
+
stacklevel=2,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
store = TokenAuth._get_store()
|
|
133
226
|
|
|
134
|
-
|
|
135
|
-
|
|
227
|
+
last = {}
|
|
228
|
+
for url, cfg in store.items():
|
|
229
|
+
if cfg.refresh_expires > last.get("refresh_expires", 0):
|
|
230
|
+
last = dict(url=url, **cfg.model_dump())
|
|
136
231
|
|
|
137
|
-
return
|
|
232
|
+
return last
|
|
138
233
|
|
|
139
234
|
def enabled(fn: Callable) -> Callable: # noqa: N805
|
|
140
235
|
"""Decorator to call or ignore a function based on the `enabled` flag."""
|
|
@@ -237,12 +332,15 @@ class TokenAuth(AuthBase):
|
|
|
237
332
|
self.log.warning("No refresh token to save.")
|
|
238
333
|
return
|
|
239
334
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
335
|
+
server_config = ServerConfig(
|
|
336
|
+
refresh_token=self.refresh_token,
|
|
337
|
+
refresh_expires=self.refresh_expires,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
with CONFIG_LOCK:
|
|
341
|
+
store = self._get_store()
|
|
342
|
+
store.update(self.url, server_config)
|
|
343
|
+
save_config(self._config_file, store.model_dump())
|
|
246
344
|
|
|
247
345
|
expire_date = datetime.fromtimestamp(self.refresh_expires, tz=timezone.utc)
|
|
248
346
|
self.log.info(
|
|
@@ -262,7 +262,7 @@ def upload_file(source: str, target: str, overwrite: bool, resume: bool, verbosi
|
|
|
262
262
|
leave=verbosity >= 2,
|
|
263
263
|
delay=0 if verbosity > 0 else 10,
|
|
264
264
|
) as pbar:
|
|
265
|
-
chunk_size = 1024 * 1024
|
|
265
|
+
chunk_size = 1024 * 1024 * 10
|
|
266
266
|
total = size
|
|
267
267
|
with open(source, "rb") as f:
|
|
268
268
|
with closing(obstore.open_writer(s3, obj.key, buffer_size=chunk_size)) as g:
|
|
@@ -331,7 +331,7 @@ def download_file(source: str, target: str, overwrite: bool, resume: bool, verbo
|
|
|
331
331
|
leave=verbosity >= 2,
|
|
332
332
|
delay=0 if verbosity > 0 else 10,
|
|
333
333
|
) as pbar:
|
|
334
|
-
chunk_size = 1024 * 1024
|
|
334
|
+
chunk_size = 1024 * 1024 * 10
|
|
335
335
|
total = size
|
|
336
336
|
with closing(obstore.open_reader(s3, obj.key, buffer_size=chunk_size)) as f:
|
|
337
337
|
with open(target, "wb") as g:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anemoi-utils
|
|
3
|
-
Version: 0.4.
|
|
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
|
|
@@ -15,6 +15,8 @@ import time
|
|
|
15
15
|
import pytest
|
|
16
16
|
|
|
17
17
|
from anemoi.utils.mlflow.auth import NoAuth
|
|
18
|
+
from anemoi.utils.mlflow.auth import ServerConfig
|
|
19
|
+
from anemoi.utils.mlflow.auth import ServerStore
|
|
18
20
|
from anemoi.utils.mlflow.auth import TokenAuth
|
|
19
21
|
|
|
20
22
|
|
|
@@ -35,17 +37,19 @@ def mocks(
|
|
|
35
37
|
response.update(token_request)
|
|
36
38
|
|
|
37
39
|
config = {
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
+
"https://test.url": {
|
|
41
|
+
"refresh_token": "old_refresh_token",
|
|
42
|
+
"refresh_expires": time.time() + 3600,
|
|
43
|
+
}
|
|
40
44
|
}
|
|
41
|
-
config.update(load_config)
|
|
45
|
+
config["https://test.url"].update(load_config)
|
|
42
46
|
|
|
43
47
|
mock_token_request = mocker.patch(
|
|
44
48
|
"anemoi.utils.mlflow.auth.TokenAuth._token_request",
|
|
45
49
|
return_value=response,
|
|
46
50
|
)
|
|
47
51
|
mocker.patch(
|
|
48
|
-
"anemoi.utils.mlflow.auth.
|
|
52
|
+
"anemoi.utils.mlflow.auth.load_raw_config",
|
|
49
53
|
return_value=config,
|
|
50
54
|
)
|
|
51
55
|
mocker.patch(
|
|
@@ -177,3 +181,108 @@ def test_noauth_methods_do_nothing():
|
|
|
177
181
|
assert auth.save() is None
|
|
178
182
|
assert auth.login() is None
|
|
179
183
|
assert auth.authenticate() is None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def test_legacy_format(mocker: pytest.MockerFixture) -> None:
|
|
187
|
+
mocks(mocker)
|
|
188
|
+
|
|
189
|
+
legacy_config = {
|
|
190
|
+
"url": "https://test.url",
|
|
191
|
+
"refresh_token": "some_refresh_token",
|
|
192
|
+
"refresh_expires": 123,
|
|
193
|
+
}
|
|
194
|
+
new_config = {
|
|
195
|
+
legacy_config["url"]: {
|
|
196
|
+
"refresh_token": legacy_config["refresh_token"],
|
|
197
|
+
"refresh_expires": legacy_config["refresh_expires"],
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
mocker.patch(
|
|
201
|
+
"anemoi.utils.mlflow.auth.load_raw_config",
|
|
202
|
+
return_value=legacy_config,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# test backwards compatibility of deprecated load_config
|
|
206
|
+
# when this function is removed, also remove this assert
|
|
207
|
+
config = TokenAuth.load_config()
|
|
208
|
+
assert config == legacy_config
|
|
209
|
+
|
|
210
|
+
# test that the store can handle both formats and the outputs are identical
|
|
211
|
+
legacy_store = ServerStore(legacy_config)
|
|
212
|
+
new_store = ServerStore(new_config)
|
|
213
|
+
expected_config = new_config["https://test.url"]
|
|
214
|
+
|
|
215
|
+
assert (
|
|
216
|
+
legacy_store["https://test.url"].model_dump() == new_store["https://test.url"].model_dump() == expected_config
|
|
217
|
+
)
|
|
218
|
+
assert legacy_store.model_dump() == new_store.model_dump() == new_config
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
multi_config = {
|
|
222
|
+
"https://server-1.url": {
|
|
223
|
+
"refresh_token": "refresh-token-1",
|
|
224
|
+
"refresh_expires": 1,
|
|
225
|
+
},
|
|
226
|
+
"https://server-3.url": {
|
|
227
|
+
"refresh_token": "refresh-token-3",
|
|
228
|
+
"refresh_expires": 3,
|
|
229
|
+
},
|
|
230
|
+
"https://server-2.url": {
|
|
231
|
+
"refresh_token": "refresh-token-2",
|
|
232
|
+
"refresh_expires": 2,
|
|
233
|
+
},
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@pytest.mark.parametrize(
|
|
238
|
+
"url, unknown",
|
|
239
|
+
[
|
|
240
|
+
("https://server-1.url", False),
|
|
241
|
+
("https://server-2.url", False),
|
|
242
|
+
("https://server-3.url", False),
|
|
243
|
+
("https://unknown.url", True),
|
|
244
|
+
],
|
|
245
|
+
)
|
|
246
|
+
def test_multi_server_format(mocker: pytest.MockerFixture, url: str, unknown: bool) -> None:
|
|
247
|
+
mocks(mocker)
|
|
248
|
+
|
|
249
|
+
mocker.patch(
|
|
250
|
+
"anemoi.utils.mlflow.auth.load_raw_config",
|
|
251
|
+
return_value=multi_config,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
auth = TokenAuth(url)
|
|
255
|
+
|
|
256
|
+
if unknown:
|
|
257
|
+
assert auth.refresh_token is None
|
|
258
|
+
assert auth.refresh_expires == 0
|
|
259
|
+
else:
|
|
260
|
+
assert auth.refresh_token == multi_config[url]["refresh_token"]
|
|
261
|
+
assert auth.refresh_expires == multi_config[url]["refresh_expires"]
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def test_server_store() -> None:
|
|
265
|
+
store = ServerStore(multi_config)
|
|
266
|
+
|
|
267
|
+
config = store["https://server-2.url"]
|
|
268
|
+
assert isinstance(config, ServerConfig)
|
|
269
|
+
assert config.refresh_token == "refresh-token-2"
|
|
270
|
+
assert config.refresh_expires == 2
|
|
271
|
+
|
|
272
|
+
assert store.get("https://unknown.url") is None
|
|
273
|
+
|
|
274
|
+
# ordered by expiry time, highest first
|
|
275
|
+
assert store.servers == [("https://server-3.url", 3), ("https://server-2.url", 2), ("https://server-1.url", 1)]
|
|
276
|
+
|
|
277
|
+
assert ServerStore({}).model_dump() == {}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_utils_interface():
|
|
281
|
+
"""TokenAuth uses the utils CONFIG_LOCK when reading and writing the server store to ensure thread safety.
|
|
282
|
+
Ensure that CONFIG_LOCK stays a reentrant lock, if it were a normal lock it would deadlock itself.
|
|
283
|
+
"""
|
|
284
|
+
from threading import RLock
|
|
285
|
+
|
|
286
|
+
from anemoi.utils.config import CONFIG_LOCK
|
|
287
|
+
|
|
288
|
+
assert isinstance(CONFIG_LOCK, type(RLock()))
|
|
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
|
{anemoi_utils-0.4.36 → anemoi_utils-0.4.37}/.github/workflows/pr-label-conventional-commits.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|