anemoi-utils 0.4.27__tar.gz → 0.4.28__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.27 → anemoi_utils-0.4.28}/.github/pull_request_template.md +2 -0
- anemoi_utils-0.4.28/.release-please-manifest.json +3 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/CHANGELOG.md +12 -0
- {anemoi_utils-0.4.27/src/anemoi_utils.egg-info → anemoi_utils-0.4.28}/PKG-INFO +6 -2
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/pyproject.toml +4 -2
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/_version.py +2 -2
- anemoi_utils-0.4.28/src/anemoi/utils/mlflow/__init__.py +8 -0
- anemoi_utils-0.4.28/src/anemoi/utils/mlflow/auth.py +246 -0
- anemoi_utils-0.4.28/src/anemoi/utils/mlflow/client.py +76 -0
- anemoi_utils-0.4.28/src/anemoi/utils/mlflow/utils.py +159 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28/src/anemoi_utils.egg-info}/PKG-INFO +6 -2
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi_utils.egg-info/SOURCES.txt +7 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi_utils.egg-info/requires.txt +6 -1
- anemoi_utils-0.4.28/tests/test_mlflow_auth.py +162 -0
- anemoi_utils-0.4.28/tests/test_mlflow_client.py +47 -0
- anemoi_utils-0.4.28/tests/test_mlflow_utils.py +76 -0
- anemoi_utils-0.4.27/.release-please-manifest.json +0 -3
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/.gitattributes +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/.github/CODEOWNERS +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/.github/ci-hpc-config.yml +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/.github/dependabot.yml +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/.github/labeler.yml +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/.github/workflows/downstream-ci-hpc.yml +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/.github/workflows/pr-conventional-commit.yml +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/.github/workflows/pr-label-conventional-commits.yml +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/.github/workflows/pr-label-file-based.yml +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/.github/workflows/pr-label-public.yml +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/.github/workflows/python-publish.yml +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/.github/workflows/python-pull-request.yml +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/.github/workflows/readthedocs-pr-update.yml +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/.github/workflows/release-please.yml +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/.gitignore +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/.pre-commit-config.yaml +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/.readthedocs.yaml +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/.release-please-config.json +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/CONTRIBUTORS.md +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/LICENSE +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/README.md +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/docs/Makefile +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/docs/_static/logo.png +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/docs/_static/style.css +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/docs/_templates/.gitkeep +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/docs/_templates/apidoc/package.rst.jinja +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/docs/conf.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/docs/index.rst +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/docs/installing.rst +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/docs/modules/checkpoints.rst +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/docs/modules/config.rst +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/docs/modules/dates.rst +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/docs/modules/grib.rst +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/docs/modules/humanize.rst +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/docs/modules/provenance.rst +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/docs/modules/s3.rst +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/docs/modules/testing.rst +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/docs/modules/text.rst +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/docs/scripts/api_build.sh +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/setup.cfg +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/__init__.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/__main__.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/caching.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/checkpoints.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/cli.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/commands/__init__.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/commands/config.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/commands/metadata.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/commands/requests.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/commands/transfer.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/compatibility.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/config.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/dates.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/devtools.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/grib.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/grids.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/hindcasts.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/humanize.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/logs.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/mars/__init__.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/mars/mars.yaml +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/mars/requests.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/provenance.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/registry.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/remote/__init__.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/remote/s3.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/remote/ssh.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/rules.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/s3.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/sanitise.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/sanitize.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/schemas/__init__.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/schemas/errors.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/testing.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/text.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi/utils/timer.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi_utils.egg-info/dependency_links.txt +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi_utils.egg-info/entry_points.txt +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/src/anemoi_utils.egg-info/top_level.txt +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/tests/test-transfer-data/directory/b/c/x +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/tests/test-transfer-data/directory/b/y +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/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.27 → anemoi_utils-0.4.28}/tests/test-transfer-data/directory/z +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/tests/test-transfer-data/file +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/tests/test_caching.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/tests/test_compatibility.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/tests/test_dates.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/tests/test_frequency.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/tests/test_provenance.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/tests/test_remote.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/tests/test_sanetise.py +0 -0
- {anemoi_utils-0.4.27 → anemoi_utils-0.4.28}/tests/test_utils.py +0 -0
|
@@ -11,3 +11,5 @@
|
|
|
11
11
|
<!-- Include any additional information, caveats, or considerations that the reviewer should be aware of. -->
|
|
12
12
|
|
|
13
13
|
***As a contributor to the Anemoi framework, please ensure that your changes include unit tests, updates to any affected dependencies and documentation, and have been tested in a parallel setting (i.e., with multiple GPUs). As a reviewer, you are also responsible for verifying these aspects and requesting changes if they are not adequately addressed. For guidelines about those please refer to https://anemoi.readthedocs.io/en/latest/***
|
|
14
|
+
|
|
15
|
+
By opening this pull request, I affirm that all authors agree to the [Contributor License Agreement.](https://github.com/ecmwf/codex/blob/main/Legal/contributor_license_agreement.md)
|
|
@@ -8,6 +8,18 @@ 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.28](https://github.com/ecmwf/anemoi-utils/compare/0.4.27...0.4.28) (2025-07-03)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* Migrate mlflow utils from anemoi-training ([#174](https://github.com/ecmwf/anemoi-utils/issues/174)) ([0b7767b](https://github.com/ecmwf/anemoi-utils/commit/0b7767bc23486b140ad7423e3c5c7d5857cef71c))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
* Treat mlflow as an optional dependency ([#177](https://github.com/ecmwf/anemoi-utils/issues/177)) ([feb1088](https://github.com/ecmwf/anemoi-utils/commit/feb1088169a29f42032bf26d5c43f9817557bafc))
|
|
22
|
+
|
|
11
23
|
## [0.4.27](https://github.com/ecmwf/anemoi-utils/compare/0.4.26...0.4.27) (2025-06-27)
|
|
12
24
|
|
|
13
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anemoi-utils
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.28
|
|
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
|
|
@@ -236,7 +236,7 @@ Requires-Dist: rich
|
|
|
236
236
|
Requires-Dist: tomli; python_version < "3.11"
|
|
237
237
|
Requires-Dist: tqdm
|
|
238
238
|
Provides-Extra: all
|
|
239
|
-
Requires-Dist: anemoi-utils[grib,provenance,s3,text]; extra == "all"
|
|
239
|
+
Requires-Dist: anemoi-utils[grib,mlflow,provenance,s3,text]; extra == "all"
|
|
240
240
|
Provides-Extra: dev
|
|
241
241
|
Requires-Dist: anemoi-utils[all,docs,tests]; extra == "dev"
|
|
242
242
|
Provides-Extra: docs
|
|
@@ -250,6 +250,9 @@ Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
|
|
250
250
|
Requires-Dist: termcolor; extra == "docs"
|
|
251
251
|
Provides-Extra: grib
|
|
252
252
|
Requires-Dist: requests; extra == "grib"
|
|
253
|
+
Provides-Extra: mlflow
|
|
254
|
+
Requires-Dist: mlflow>=2.11.1; extra == "mlflow"
|
|
255
|
+
Requires-Dist: requests; extra == "mlflow"
|
|
253
256
|
Provides-Extra: provenance
|
|
254
257
|
Requires-Dist: gitpython; extra == "provenance"
|
|
255
258
|
Requires-Dist: nvsmi; extra == "provenance"
|
|
@@ -257,6 +260,7 @@ Provides-Extra: s3
|
|
|
257
260
|
Requires-Dist: boto3>1.36; extra == "s3"
|
|
258
261
|
Provides-Extra: tests
|
|
259
262
|
Requires-Dist: pytest; extra == "tests"
|
|
263
|
+
Requires-Dist: pytest-mock>=3; extra == "tests"
|
|
260
264
|
Provides-Extra: text
|
|
261
265
|
Requires-Dist: termcolor; extra == "text"
|
|
262
266
|
Requires-Dist: wcwidth; extra == "text"
|
|
@@ -53,7 +53,7 @@ dependencies = [
|
|
|
53
53
|
"tqdm",
|
|
54
54
|
]
|
|
55
55
|
|
|
56
|
-
optional-dependencies.all = [ "anemoi-utils[grib,provenance,text,s3]" ]
|
|
56
|
+
optional-dependencies.all = [ "anemoi-utils[grib,provenance,text,s3,mlflow]" ]
|
|
57
57
|
optional-dependencies.dev = [ "anemoi-utils[all,docs,tests]" ]
|
|
58
58
|
|
|
59
59
|
optional-dependencies.docs = [
|
|
@@ -69,13 +69,15 @@ optional-dependencies.docs = [
|
|
|
69
69
|
|
|
70
70
|
optional-dependencies.grib = [ "requests" ]
|
|
71
71
|
|
|
72
|
+
optional-dependencies.mlflow = [ "mlflow>=2.11.1", "requests" ]
|
|
73
|
+
|
|
72
74
|
optional-dependencies.provenance = [ "gitpython", "nvsmi" ]
|
|
73
75
|
|
|
74
76
|
optional-dependencies.s3 = [
|
|
75
77
|
"boto3>1.36",
|
|
76
78
|
]
|
|
77
79
|
|
|
78
|
-
optional-dependencies.tests = [ "pytest" ]
|
|
80
|
+
optional-dependencies.tests = [ "pytest", "pytest-mock>=3" ]
|
|
79
81
|
|
|
80
82
|
optional-dependencies.text = [ "termcolor", "wcwidth" ]
|
|
81
83
|
|
|
@@ -0,0 +1,8 @@
|
|
|
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.
|
|
@@ -0,0 +1,246 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import time
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
from datetime import timezone
|
|
18
|
+
from functools import wraps
|
|
19
|
+
from getpass import getpass
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
import requests
|
|
23
|
+
from requests.exceptions import HTTPError
|
|
24
|
+
|
|
25
|
+
from ..config import load_config
|
|
26
|
+
from ..config import save_config
|
|
27
|
+
from ..remote import robust
|
|
28
|
+
from ..timer import Timer
|
|
29
|
+
|
|
30
|
+
REFRESH_EXPIRE_DAYS = 29
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from collections.abc import Callable
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TokenAuth:
|
|
38
|
+
"""Manage authentication with a keycloak token server."""
|
|
39
|
+
|
|
40
|
+
config_file = "mlflow-token.json"
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
url: str,
|
|
45
|
+
enabled: bool = True,
|
|
46
|
+
target_env_var: str = "MLFLOW_TRACKING_TOKEN",
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Initialise the token authentication object.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
url : str
|
|
53
|
+
URL of the authentication server.
|
|
54
|
+
enabled : bool, optional
|
|
55
|
+
Set this to False to turn off authentication, by default True
|
|
56
|
+
target_env_var : str, optional
|
|
57
|
+
The environment variable to store the access token in after authenticating,
|
|
58
|
+
by default `MLFLOW_TRACKING_TOKEN`
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
self.url = url
|
|
62
|
+
self.target_env_var = target_env_var
|
|
63
|
+
self._enabled = enabled
|
|
64
|
+
|
|
65
|
+
config = self.load_config()
|
|
66
|
+
|
|
67
|
+
self._refresh_token = config.get("refresh_token")
|
|
68
|
+
self.refresh_expires = config.get("refresh_expires", 0)
|
|
69
|
+
self.access_token = None
|
|
70
|
+
self.access_expires = 0
|
|
71
|
+
|
|
72
|
+
# the command line tool adds a default handler to the root logger on runtime,
|
|
73
|
+
# so we init our logger here (on runtime, not on import) to avoid duplicate handlers
|
|
74
|
+
self.log = logging.getLogger(__name__)
|
|
75
|
+
|
|
76
|
+
def __call__(self) -> None:
|
|
77
|
+
self.authenticate()
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def refresh_token(self) -> str:
|
|
81
|
+
return self._refresh_token
|
|
82
|
+
|
|
83
|
+
@refresh_token.setter
|
|
84
|
+
def refresh_token(self, value: str) -> None:
|
|
85
|
+
self._refresh_token = value
|
|
86
|
+
self.refresh_expires = time.time() + (REFRESH_EXPIRE_DAYS * 86400) # 86400 seconds in a day
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def load_config() -> dict:
|
|
90
|
+
return load_config(TokenAuth.config_file)
|
|
91
|
+
|
|
92
|
+
def enabled(fn: Callable) -> Callable: # noqa: N805
|
|
93
|
+
"""Decorator to call or ignore a function based on the `enabled` flag."""
|
|
94
|
+
|
|
95
|
+
@wraps(fn)
|
|
96
|
+
def _wrapper(self: TokenAuth, *args, **kwargs) -> Callable | None:
|
|
97
|
+
if self._enabled:
|
|
98
|
+
return fn(self, *args, **kwargs)
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
return _wrapper
|
|
102
|
+
|
|
103
|
+
@enabled
|
|
104
|
+
def login(self, force_credentials: bool = False, **kwargs: dict) -> None:
|
|
105
|
+
"""Acquire a new refresh token and save it to disk.
|
|
106
|
+
|
|
107
|
+
If an existing valid refresh token is already on disk it will be used.
|
|
108
|
+
If not, or the token has expired, the user will be asked to obtain one from the API.
|
|
109
|
+
|
|
110
|
+
Refresh token expiry time is set in the `REFRESH_EXPIRE_DAYS` constant (default 29 days).
|
|
111
|
+
|
|
112
|
+
This function should be called once, interactively, right before starting a training run.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
force_credentials : bool, optional
|
|
117
|
+
Force a credential login even if a refreh token is available, by default False.
|
|
118
|
+
kwargs : dict
|
|
119
|
+
Additional keyword arguments.
|
|
120
|
+
|
|
121
|
+
Raises
|
|
122
|
+
------
|
|
123
|
+
RuntimeError
|
|
124
|
+
A new refresh token could not be acquired.
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
del kwargs # unused
|
|
128
|
+
self.log.info("🌐 Logging in to %s", self.url)
|
|
129
|
+
new_refresh_token = None
|
|
130
|
+
|
|
131
|
+
if not force_credentials and self.refresh_token and self.refresh_expires > time.time():
|
|
132
|
+
new_refresh_token = self._token_request(ignore_exc=True).get("refresh_token")
|
|
133
|
+
|
|
134
|
+
if not new_refresh_token:
|
|
135
|
+
self.log.info("📝 Please obtain a seed refresh token from %s/seed", self.url)
|
|
136
|
+
self.log.info("📝 and paste it here (you will not see the output, just press enter after pasting):")
|
|
137
|
+
self.refresh_token = getpass("Refresh Token: ")
|
|
138
|
+
|
|
139
|
+
# perform a new refresh token request to check if the seed refresh token is valid
|
|
140
|
+
new_refresh_token = self._token_request().get("refresh_token")
|
|
141
|
+
|
|
142
|
+
if not new_refresh_token:
|
|
143
|
+
msg = "❌ Failed to log in. Please try again."
|
|
144
|
+
raise RuntimeError(msg)
|
|
145
|
+
|
|
146
|
+
self.refresh_token = new_refresh_token
|
|
147
|
+
self.save()
|
|
148
|
+
|
|
149
|
+
self.log.info("✅ Successfully logged in to MLflow. Happy logging!")
|
|
150
|
+
|
|
151
|
+
@enabled
|
|
152
|
+
def authenticate(self, **kwargs: dict) -> None:
|
|
153
|
+
"""Check the access token and refresh it if necessary. A new refresh token will also be acquired upon refresh.
|
|
154
|
+
|
|
155
|
+
This requires a valid refresh token to be available, obtained from the `login` method.
|
|
156
|
+
|
|
157
|
+
The access token is stored in memory and in an environment variable.
|
|
158
|
+
If the access token is still valid, this function does nothing.
|
|
159
|
+
|
|
160
|
+
This function should be called before every MLflow API request.
|
|
161
|
+
|
|
162
|
+
Raises
|
|
163
|
+
------
|
|
164
|
+
RuntimeError
|
|
165
|
+
No refresh token is available or the token request failed.
|
|
166
|
+
|
|
167
|
+
"""
|
|
168
|
+
del kwargs # unused
|
|
169
|
+
if self.access_expires > time.time():
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
if not self.refresh_token or self.refresh_expires < time.time():
|
|
173
|
+
msg = "You are not logged in to MLflow. Please log in first."
|
|
174
|
+
raise RuntimeError(msg)
|
|
175
|
+
|
|
176
|
+
with Timer("Access token refreshed", self.log):
|
|
177
|
+
response = self._token_request()
|
|
178
|
+
|
|
179
|
+
self.access_token = response.get("access_token")
|
|
180
|
+
self.access_expires = time.time() + (response.get("expires_in") * 0.7) # bit of buffer
|
|
181
|
+
self.refresh_token = response.get("refresh_token")
|
|
182
|
+
|
|
183
|
+
os.environ[self.target_env_var] = self.access_token
|
|
184
|
+
|
|
185
|
+
@enabled
|
|
186
|
+
def save(self, **kwargs: dict) -> None:
|
|
187
|
+
"""Save the latest refresh token to disk."""
|
|
188
|
+
del kwargs # unused
|
|
189
|
+
if not self.refresh_token:
|
|
190
|
+
self.log.warning("No refresh token to save.")
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
config = {
|
|
194
|
+
"url": self.url,
|
|
195
|
+
"refresh_token": self.refresh_token,
|
|
196
|
+
"refresh_expires": self.refresh_expires,
|
|
197
|
+
}
|
|
198
|
+
save_config(self.config_file, config)
|
|
199
|
+
|
|
200
|
+
expire_date = datetime.fromtimestamp(self.refresh_expires, tz=timezone.utc)
|
|
201
|
+
self.log.info(
|
|
202
|
+
"Your MLflow login token is valid until %s UTC",
|
|
203
|
+
expire_date.strftime("%Y-%m-%d %H:%M:%S"),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def _token_request(
|
|
207
|
+
self,
|
|
208
|
+
ignore_exc: bool = False,
|
|
209
|
+
) -> dict:
|
|
210
|
+
path = "refreshtoken"
|
|
211
|
+
payload = {"refresh_token": self.refresh_token}
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
response = self._request(path, payload)
|
|
215
|
+
except Exception:
|
|
216
|
+
if ignore_exc:
|
|
217
|
+
return {}
|
|
218
|
+
raise
|
|
219
|
+
|
|
220
|
+
return response
|
|
221
|
+
|
|
222
|
+
def _request(self, path: str, payload: dict) -> dict:
|
|
223
|
+
|
|
224
|
+
headers = {
|
|
225
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
response = robust(requests.post)(
|
|
230
|
+
f"{self.url}/{path}",
|
|
231
|
+
headers=headers,
|
|
232
|
+
json=payload,
|
|
233
|
+
timeout=60,
|
|
234
|
+
)
|
|
235
|
+
response.raise_for_status()
|
|
236
|
+
response_json = response.json()
|
|
237
|
+
|
|
238
|
+
if response_json.get("status", "") != "OK":
|
|
239
|
+
error_description = response_json.get("response", "Error acquiring token.")
|
|
240
|
+
msg = f"❌ {error_description}"
|
|
241
|
+
raise RuntimeError(msg)
|
|
242
|
+
|
|
243
|
+
return response_json["response"]
|
|
244
|
+
except HTTPError:
|
|
245
|
+
self.log.exception("HTTP error occurred")
|
|
246
|
+
raise
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from mlflow import MlflowClient
|
|
17
|
+
except ImportError:
|
|
18
|
+
raise ImportError(
|
|
19
|
+
"The `mlflow` package is required to use AnemoiMLflowclient. Please install it with `pip install mlflow`."
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .auth import TokenAuth
|
|
23
|
+
from .utils import health_check
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AnemoiMlflowClient(MlflowClient):
|
|
27
|
+
"""Anemoi extension of the MLflow client with token authentication support."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
tracking_uri: str,
|
|
32
|
+
*args,
|
|
33
|
+
authentication: bool = False,
|
|
34
|
+
check_health: bool = True,
|
|
35
|
+
**kwargs,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Behaves like a normal `mlflow.MlflowClient` but with token authentication injected on every call.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
tracking_uri : str
|
|
42
|
+
The URI of the MLflow tracking server.
|
|
43
|
+
authentication : bool, optional
|
|
44
|
+
Enable token authentication, by default False
|
|
45
|
+
check_health : bool, optional
|
|
46
|
+
Check the health of the MLflow server on init, by default True
|
|
47
|
+
*args : Any
|
|
48
|
+
Additional arguments to pass to the MLflow client.
|
|
49
|
+
**kwargs : Any
|
|
50
|
+
Additional keyword arguments to pass to the MLflow client.
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
self.anemoi_auth = TokenAuth(tracking_uri, enabled=authentication)
|
|
54
|
+
if check_health:
|
|
55
|
+
super().__getattribute__("anemoi_auth").authenticate()
|
|
56
|
+
health_check(tracking_uri)
|
|
57
|
+
super().__init__(tracking_uri, *args, **kwargs)
|
|
58
|
+
|
|
59
|
+
def __getattribute__(self, name: str) -> Any:
|
|
60
|
+
"""Intercept attribute access and inject authentication."""
|
|
61
|
+
attr = super().__getattribute__(name)
|
|
62
|
+
if callable(attr) and name != "anemoi_auth":
|
|
63
|
+
super().__getattribute__("anemoi_auth").authenticate()
|
|
64
|
+
return attr
|
|
65
|
+
|
|
66
|
+
def login(self, force_credentials: bool = False, **kwargs) -> None:
|
|
67
|
+
"""Explicitly log in to the MLflow server by acquiring or refreshing the token.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
force_credentials : bool, optional
|
|
72
|
+
Force a credential login even if a refresh token is available, by default False.
|
|
73
|
+
kwargs : dict
|
|
74
|
+
Additional keyword arguments passed to the underlying TokenAuth.login.
|
|
75
|
+
"""
|
|
76
|
+
self.anemoi_auth.login(force_credentials=force_credentials, **kwargs)
|
|
@@ -0,0 +1,159 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import functools
|
|
12
|
+
import os
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
from ..remote import robust
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def health_check(tracking_uri: str) -> None:
|
|
21
|
+
"""Query the health endpoint of an MLflow server.
|
|
22
|
+
|
|
23
|
+
If the server is not reachable, raise an error and remind the user that authentication may be required.
|
|
24
|
+
|
|
25
|
+
Raises
|
|
26
|
+
------
|
|
27
|
+
ConnectionError
|
|
28
|
+
If the server is not reachable.
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
token = os.getenv("MLFLOW_TRACKING_TOKEN")
|
|
32
|
+
|
|
33
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
34
|
+
response = robust(requests.get, retry_after=30, maximum_tries=10)(
|
|
35
|
+
f"{tracking_uri}/health",
|
|
36
|
+
headers=headers,
|
|
37
|
+
timeout=60,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if response.text == "OK":
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
error_msg = f"Could not connect to MLflow server at {tracking_uri}. "
|
|
44
|
+
if not token:
|
|
45
|
+
error_msg += "The server may require authentication, did you forget to turn it on?"
|
|
46
|
+
raise ConnectionError(error_msg)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def expand_iterables(
|
|
50
|
+
params: dict[str, Any],
|
|
51
|
+
*,
|
|
52
|
+
size_threshold: int | None = None,
|
|
53
|
+
recursive: bool = True,
|
|
54
|
+
delimiter: str = ".",
|
|
55
|
+
) -> dict[str, Any]:
|
|
56
|
+
"""Expand any iterable values to the form {key.i: value_i}.
|
|
57
|
+
|
|
58
|
+
If expanded will also add {key.all: [value_0, value_1, ...], key.length: len([value_0, value_1, ...])}.
|
|
59
|
+
|
|
60
|
+
If `size_threshold` is not None, expand the iterable only if the length of str(value) is
|
|
61
|
+
greater than `size_threshold`.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
params : dict[str, Any]
|
|
66
|
+
Parameters to be expanded.
|
|
67
|
+
size_threshold : int | None, optional
|
|
68
|
+
Threshold of str(value) to expand iterable at.
|
|
69
|
+
Default is None.
|
|
70
|
+
recursive : bool, optional
|
|
71
|
+
Expand nested dictionaries.
|
|
72
|
+
Default is True.
|
|
73
|
+
delimiter: str, optional
|
|
74
|
+
Delimiter to use for keys.
|
|
75
|
+
Default is ".".
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
dict[str, Any]
|
|
80
|
+
Dictionary with all iterable values expanded.
|
|
81
|
+
|
|
82
|
+
Examples
|
|
83
|
+
--------
|
|
84
|
+
>>> expand_iterables({'a': ['a', 'b', 'c']})
|
|
85
|
+
{'a.0': 'a', 'a.1': 'b', 'a.2': 'c', 'a.all': ['a', 'b', 'c'], 'a.length': 3}
|
|
86
|
+
>>> expand_iterables({'a': {'b': ['a', 'b', 'c']}})
|
|
87
|
+
{'a': {'b.0': 'a', 'b.1': 'b', 'b.2': 'c', 'b.all': ['a', 'b', 'c'], 'b.length': 3}}
|
|
88
|
+
>>> expand_iterables({'a': ['a', 'b', 'c']}, size_threshold=100)
|
|
89
|
+
{'a': ['a', 'b', 'c']}
|
|
90
|
+
>>> expand_iterables({'a': [[0,1,2], 'b', 'c']})
|
|
91
|
+
{'a.0': {0: 0, 1: 1, 2: 2}, 'a.1': 'b', 'a.2': 'c', 'a.all': [[0, 1, 2], 'b', 'c'], 'a.length': 3}
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def should_be_expanded(x: Any) -> bool:
|
|
95
|
+
return size_threshold is None or len(str(x)) > size_threshold
|
|
96
|
+
|
|
97
|
+
nested_func = functools.partial(expand_iterables, size_threshold=size_threshold, recursive=recursive)
|
|
98
|
+
|
|
99
|
+
def expand(val: dict | list) -> dict[str, Any]:
|
|
100
|
+
if not recursive:
|
|
101
|
+
return val
|
|
102
|
+
if isinstance(val, dict):
|
|
103
|
+
return nested_func(val)
|
|
104
|
+
if isinstance(val, list):
|
|
105
|
+
return nested_func(dict(enumerate(val)))
|
|
106
|
+
return val
|
|
107
|
+
|
|
108
|
+
expanded_params = {}
|
|
109
|
+
|
|
110
|
+
for key, value in params.items():
|
|
111
|
+
if isinstance(value, (list, tuple)):
|
|
112
|
+
if should_be_expanded(value):
|
|
113
|
+
for i, v in enumerate(value):
|
|
114
|
+
expanded_params[f"{key}{delimiter}{i}"] = expand(v)
|
|
115
|
+
|
|
116
|
+
expanded_params[f"{key}{delimiter}all"] = value
|
|
117
|
+
expanded_params[f"{key}{delimiter}length"] = len(value)
|
|
118
|
+
else:
|
|
119
|
+
expanded_params[key] = value
|
|
120
|
+
else:
|
|
121
|
+
expanded_params[key] = expand(value)
|
|
122
|
+
return expanded_params
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def clean_config_params(params: dict[str, Any]) -> dict[str, Any]:
|
|
126
|
+
"""Clean up params to avoid issues with mlflow.
|
|
127
|
+
|
|
128
|
+
Too many logged params will make the server take longer to render the
|
|
129
|
+
experiment.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
params : dict[str, Any]
|
|
134
|
+
Parameters to clean up.
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
dict[str, Any]
|
|
139
|
+
Cleaned up params ready for MlFlow.
|
|
140
|
+
"""
|
|
141
|
+
prefixes_to_remove = [
|
|
142
|
+
"hardware",
|
|
143
|
+
"data",
|
|
144
|
+
"dataloader",
|
|
145
|
+
"model",
|
|
146
|
+
"training",
|
|
147
|
+
"diagnostics",
|
|
148
|
+
"graph",
|
|
149
|
+
"metadata.config",
|
|
150
|
+
"config.dataset.sourcesmetadata.dataset.variables_metadata",
|
|
151
|
+
"metadata.dataset.sources",
|
|
152
|
+
"metadata.dataset.specific",
|
|
153
|
+
"metadata.dataset.variables_metadata",
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
keys_to_remove = [key for key in params if any(key.startswith(prefix) for prefix in prefixes_to_remove)]
|
|
157
|
+
for key in keys_to_remove:
|
|
158
|
+
del params[key]
|
|
159
|
+
return params
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anemoi-utils
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.28
|
|
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
|
|
@@ -236,7 +236,7 @@ Requires-Dist: rich
|
|
|
236
236
|
Requires-Dist: tomli; python_version < "3.11"
|
|
237
237
|
Requires-Dist: tqdm
|
|
238
238
|
Provides-Extra: all
|
|
239
|
-
Requires-Dist: anemoi-utils[grib,provenance,s3,text]; extra == "all"
|
|
239
|
+
Requires-Dist: anemoi-utils[grib,mlflow,provenance,s3,text]; extra == "all"
|
|
240
240
|
Provides-Extra: dev
|
|
241
241
|
Requires-Dist: anemoi-utils[all,docs,tests]; extra == "dev"
|
|
242
242
|
Provides-Extra: docs
|
|
@@ -250,6 +250,9 @@ Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
|
|
250
250
|
Requires-Dist: termcolor; extra == "docs"
|
|
251
251
|
Provides-Extra: grib
|
|
252
252
|
Requires-Dist: requests; extra == "grib"
|
|
253
|
+
Provides-Extra: mlflow
|
|
254
|
+
Requires-Dist: mlflow>=2.11.1; extra == "mlflow"
|
|
255
|
+
Requires-Dist: requests; extra == "mlflow"
|
|
253
256
|
Provides-Extra: provenance
|
|
254
257
|
Requires-Dist: gitpython; extra == "provenance"
|
|
255
258
|
Requires-Dist: nvsmi; extra == "provenance"
|
|
@@ -257,6 +260,7 @@ Provides-Extra: s3
|
|
|
257
260
|
Requires-Dist: boto3>1.36; extra == "s3"
|
|
258
261
|
Provides-Extra: tests
|
|
259
262
|
Requires-Dist: pytest; extra == "tests"
|
|
263
|
+
Requires-Dist: pytest-mock>=3; extra == "tests"
|
|
260
264
|
Provides-Extra: text
|
|
261
265
|
Requires-Dist: termcolor; extra == "text"
|
|
262
266
|
Requires-Dist: wcwidth; extra == "text"
|
|
@@ -73,6 +73,10 @@ src/anemoi/utils/commands/transfer.py
|
|
|
73
73
|
src/anemoi/utils/mars/__init__.py
|
|
74
74
|
src/anemoi/utils/mars/mars.yaml
|
|
75
75
|
src/anemoi/utils/mars/requests.py
|
|
76
|
+
src/anemoi/utils/mlflow/__init__.py
|
|
77
|
+
src/anemoi/utils/mlflow/auth.py
|
|
78
|
+
src/anemoi/utils/mlflow/client.py
|
|
79
|
+
src/anemoi/utils/mlflow/utils.py
|
|
76
80
|
src/anemoi/utils/remote/__init__.py
|
|
77
81
|
src/anemoi/utils/remote/s3.py
|
|
78
82
|
src/anemoi/utils/remote/ssh.py
|
|
@@ -88,6 +92,9 @@ tests/test_caching.py
|
|
|
88
92
|
tests/test_compatibility.py
|
|
89
93
|
tests/test_dates.py
|
|
90
94
|
tests/test_frequency.py
|
|
95
|
+
tests/test_mlflow_auth.py
|
|
96
|
+
tests/test_mlflow_client.py
|
|
97
|
+
tests/test_mlflow_utils.py
|
|
91
98
|
tests/test_provenance.py
|
|
92
99
|
tests/test_remote.py
|
|
93
100
|
tests/test_sanetise.py
|
|
@@ -15,7 +15,7 @@ importlib-metadata
|
|
|
15
15
|
tomli
|
|
16
16
|
|
|
17
17
|
[all]
|
|
18
|
-
anemoi-utils[grib,provenance,s3,text]
|
|
18
|
+
anemoi-utils[grib,mlflow,provenance,s3,text]
|
|
19
19
|
|
|
20
20
|
[dev]
|
|
21
21
|
anemoi-utils[all,docs,tests]
|
|
@@ -33,6 +33,10 @@ termcolor
|
|
|
33
33
|
[grib]
|
|
34
34
|
requests
|
|
35
35
|
|
|
36
|
+
[mlflow]
|
|
37
|
+
mlflow>=2.11.1
|
|
38
|
+
requests
|
|
39
|
+
|
|
36
40
|
[provenance]
|
|
37
41
|
gitpython
|
|
38
42
|
nvsmi
|
|
@@ -42,6 +46,7 @@ boto3>1.36
|
|
|
42
46
|
|
|
43
47
|
[tests]
|
|
44
48
|
pytest
|
|
49
|
+
pytest-mock>=3
|
|
45
50
|
|
|
46
51
|
[text]
|
|
47
52
|
termcolor
|