hydraflow 0.7.3__tar.gz → 0.7.5__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- hydraflow-0.7.5/.github/workflows/ci.yaml +51 -0
- hydraflow-0.7.5/.github/workflows/docs.yaml +25 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/PKG-INFO +5 -2
- {hydraflow-0.7.3 → hydraflow-0.7.5}/README.md +1 -1
- hydraflow-0.7.5/docs/index.md +10 -0
- hydraflow-0.7.5/docs/usage/quickstart.md +150 -0
- hydraflow-0.7.5/hydraflow.yaml +5 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/pyproject.toml +12 -17
- {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/__init__.py +9 -1
- hydraflow-0.7.5/src/hydraflow/cli.py +75 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/mlflow.py +87 -32
- hydraflow-0.7.5/tests/cli/conftest.py +9 -0
- hydraflow-0.7.5/tests/cli/test_run.py +18 -0
- hydraflow-0.7.5/tests/cli/test_show.py +52 -0
- hydraflow-0.7.5/tests/cli/test_version.py +20 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/test_mlflow.py +7 -0
- hydraflow-0.7.5/tests/utils/__init__.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/.devcontainer/devcontainer.json +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/.devcontainer/postCreate.sh +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/.devcontainer/starship.toml +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/.gitattributes +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/.gitignore +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/LICENSE +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/apps/quickstart.py +0 -0
- /hydraflow-0.7.3/mkdocs.yml → /hydraflow-0.7.5/mkdocs.yaml +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/config.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/context.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/main.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/param.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/py.typed +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/run_collection.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/run_data.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/run_info.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/utils.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/__init__.py +0 -0
- {hydraflow-0.7.3/tests/config → hydraflow-0.7.5/tests/cli}/__init__.py +0 -0
- {hydraflow-0.7.3/tests/context → hydraflow-0.7.5/tests/config}/__init__.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/config/overrides.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/config/test_config.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/config/test_overrides.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/config/test_params.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/conftest.py +0 -0
- {hydraflow-0.7.3/tests/main → hydraflow-0.7.5/tests/context}/__init__.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/context/chdir.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/context/context.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/context/logging.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/context/rerun.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/context/test_chdir.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/context/test_context.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/context/test_logging.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/context/test_rerun.py +0 -0
- {hydraflow-0.7.3/tests/param → hydraflow-0.7.5/tests/main}/__init__.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/main/base.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/main/force_new_run.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/main/restart.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/main/skip.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/main/test_base.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/main/test_force_new_run.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/main/test_restart.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/main/test_skip.py +0 -0
- {hydraflow-0.7.3/tests/run → hydraflow-0.7.5/tests/param}/__init__.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/param/params.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/param/test_param.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/param/test_params.py +0 -0
- {hydraflow-0.7.3/tests/utils → hydraflow-0.7.5/tests/run}/__init__.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/run/filter.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/run/run.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/run/test_collection.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/run/test_data.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/run/test_filter.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/run/test_info.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/run/test_run.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/utils/test_run.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/utils/test_utils.py +0 -0
- {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/utils/utils.py +0 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [main]
|
6
|
+
pull_request:
|
7
|
+
|
8
|
+
concurrency:
|
9
|
+
group: test-${{ github.head_ref }}
|
10
|
+
cancel-in-progress: true
|
11
|
+
|
12
|
+
env:
|
13
|
+
PYTHONUNBUFFERED: "1"
|
14
|
+
FORCE_COLOR: "1"
|
15
|
+
|
16
|
+
jobs:
|
17
|
+
run:
|
18
|
+
name: Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }}
|
19
|
+
runs-on: ${{ matrix.os }}
|
20
|
+
strategy:
|
21
|
+
fail-fast: false
|
22
|
+
matrix:
|
23
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
24
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
25
|
+
|
26
|
+
steps:
|
27
|
+
- uses: actions/checkout@v4
|
28
|
+
- name: Set up Python ${{ matrix.python-version }}
|
29
|
+
uses: actions/setup-python@v5
|
30
|
+
with:
|
31
|
+
python-version: ${{ matrix.python-version }}
|
32
|
+
allow-prereleases: true
|
33
|
+
- name: Install uv and ruff
|
34
|
+
run: pip install uv ruff
|
35
|
+
- name: Install the project
|
36
|
+
run: uv sync
|
37
|
+
- name: Ruff check
|
38
|
+
run: ruff check
|
39
|
+
- name: Run test
|
40
|
+
run: uv run pytest -v --junitxml=junit.xml
|
41
|
+
- name: Upload Codecov Results
|
42
|
+
if: success()
|
43
|
+
uses: codecov/codecov-action@v4
|
44
|
+
with:
|
45
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
46
|
+
file: lcov.info
|
47
|
+
- name: Upload test results to Codecov
|
48
|
+
if: ${{ !cancelled() }}
|
49
|
+
uses: codecov/test-results-action@v1
|
50
|
+
with:
|
51
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
name: Documentation
|
2
|
+
on:
|
3
|
+
push:
|
4
|
+
branches: [main]
|
5
|
+
tags: ["*"]
|
6
|
+
permissions:
|
7
|
+
contents: write
|
8
|
+
jobs:
|
9
|
+
deploy:
|
10
|
+
name: Documentation
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
steps:
|
13
|
+
- uses: actions/checkout@v4
|
14
|
+
- name: Configure Git Credentials
|
15
|
+
run: |
|
16
|
+
git config user.name github-actions[bot]
|
17
|
+
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
18
|
+
- name: Set up Python 3.11
|
19
|
+
uses: actions/setup-python@v5
|
20
|
+
with:
|
21
|
+
python-version: 3.11
|
22
|
+
- name: Install package
|
23
|
+
run: pip install -e . mkapi markdown-exec[ansi]
|
24
|
+
- name: Deploy documentation
|
25
|
+
run: mkdocs gh-deploy --force
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hydraflow
|
3
|
-
Version: 0.7.
|
3
|
+
Version: 0.7.5
|
4
4
|
Summary: Hydraflow integrates Hydra and MLflow to manage and track machine learning experiments.
|
5
5
|
Project-URL: Documentation, https://daizutabi.github.io/hydraflow/
|
6
6
|
Project-URL: Source, https://github.com/daizutabi/hydraflow
|
@@ -38,6 +38,9 @@ Classifier: Programming Language :: Python :: 3.13
|
|
38
38
|
Requires-Python: >=3.10
|
39
39
|
Requires-Dist: hydra-core>=1.3
|
40
40
|
Requires-Dist: mlflow>=2.15
|
41
|
+
Requires-Dist: omegaconf
|
42
|
+
Requires-Dist: rich
|
43
|
+
Requires-Dist: typer
|
41
44
|
Description-Content-Type: text/markdown
|
42
45
|
|
43
46
|
# Hydraflow
|
@@ -52,7 +55,7 @@ Description-Content-Type: text/markdown
|
|
52
55
|
[pypi-v-link]: https://pypi.org/project/hydraflow/
|
53
56
|
[python-v-image]: https://img.shields.io/pypi/pyversions/hydraflow.svg
|
54
57
|
[python-v-link]: https://pypi.org/project/hydraflow
|
55
|
-
[GHAction-image]: https://github.com/daizutabi/hydraflow/actions/workflows/ci.
|
58
|
+
[GHAction-image]: https://github.com/daizutabi/hydraflow/actions/workflows/ci.yaml/badge.svg?branch=main&event=push
|
56
59
|
[GHAction-link]: https://github.com/daizutabi/hydraflow/actions?query=event%3Apush+branch%3Amain
|
57
60
|
[codecov-image]: https://codecov.io/github/daizutabi/hydraflow/coverage.svg?branch=main
|
58
61
|
[codecov-link]: https://codecov.io/github/daizutabi/hydraflow?branch=main
|
@@ -10,7 +10,7 @@
|
|
10
10
|
[pypi-v-link]: https://pypi.org/project/hydraflow/
|
11
11
|
[python-v-image]: https://img.shields.io/pypi/pyversions/hydraflow.svg
|
12
12
|
[python-v-link]: https://pypi.org/project/hydraflow
|
13
|
-
[GHAction-image]: https://github.com/daizutabi/hydraflow/actions/workflows/ci.
|
13
|
+
[GHAction-image]: https://github.com/daizutabi/hydraflow/actions/workflows/ci.yaml/badge.svg?branch=main&event=push
|
14
14
|
[GHAction-link]: https://github.com/daizutabi/hydraflow/actions?query=event%3Apush+branch%3Amain
|
15
15
|
[codecov-image]: https://codecov.io/github/daizutabi/hydraflow/coverage.svg?branch=main
|
16
16
|
[codecov-link]: https://codecov.io/github/daizutabi/hydraflow?branch=main
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# Quickstart
|
2
|
+
|
3
|
+
## Hydra application
|
4
|
+
|
5
|
+
The following example demonstrates how to use Hydraflow with a Hydra application.
|
6
|
+
There are two main steps to using Hydraflow:
|
7
|
+
|
8
|
+
1. Set the MLflow experiment using the Hydra job name.
|
9
|
+
2. Start a new MLflow run that logs the Hydra configuration.
|
10
|
+
|
11
|
+
```python title="apps/quickstart.py" linenums="1" hl_lines="24 26"
|
12
|
+
--8<-- "apps/quickstart.py"
|
13
|
+
```
|
14
|
+
|
15
|
+
### Set the MLflow experiment
|
16
|
+
|
17
|
+
[`hydraflow.set_experiment`][] sets the MLflow experiment using the Hydra job name.
|
18
|
+
Optionally, it can also set the tracking URI with `uri` argument.
|
19
|
+
For example,
|
20
|
+
|
21
|
+
```python
|
22
|
+
hydraflow.set_experiment(uri="sqlite:///mlruns.db")
|
23
|
+
```
|
24
|
+
|
25
|
+
### Start a new MLflow run
|
26
|
+
|
27
|
+
[`hydraflow.start_run`][] starts a new MLflow run that logs the Hydra configuration.
|
28
|
+
It returns the started run so that it can be used to log metrics, parameters, and artifacts
|
29
|
+
within the context of the run.
|
30
|
+
|
31
|
+
```python
|
32
|
+
with hydraflow.start_run(cfg) as run:
|
33
|
+
pass
|
34
|
+
```
|
35
|
+
|
36
|
+
## Run the application
|
37
|
+
|
38
|
+
```bash exec="on"
|
39
|
+
rm -rf mlruns outputs multirun
|
40
|
+
```
|
41
|
+
|
42
|
+
### Single-run
|
43
|
+
|
44
|
+
Run the Hydra application as a normal Python script.
|
45
|
+
|
46
|
+
```console exec="1" source="console"
|
47
|
+
$ python apps/quickstart.py
|
48
|
+
```
|
49
|
+
|
50
|
+
Check the MLflow CLI to view the experiment.
|
51
|
+
|
52
|
+
```console exec="1" source="console"
|
53
|
+
$ mlflow experiments search
|
54
|
+
```
|
55
|
+
|
56
|
+
### Multi-run
|
57
|
+
|
58
|
+
```console exec="1" source="console"
|
59
|
+
$ python apps/quickstart.py -m width=400,600 height=100,200,300
|
60
|
+
```
|
61
|
+
|
62
|
+
## Use Hydraflow API
|
63
|
+
|
64
|
+
### Run collection
|
65
|
+
|
66
|
+
```pycon exec="1" source="console" session="quickstart"
|
67
|
+
>>> import mlflow
|
68
|
+
>>> mlflow.set_experiment("quickstart")
|
69
|
+
>>> import hydraflow
|
70
|
+
>>> rc = hydraflow.list_runs()
|
71
|
+
>>> print(rc)
|
72
|
+
```
|
73
|
+
|
74
|
+
### Retrieve a run
|
75
|
+
|
76
|
+
```pycon exec="1" source="console" session="quickstart"
|
77
|
+
>>> run = rc.first()
|
78
|
+
>>> print(type(run))
|
79
|
+
```
|
80
|
+
|
81
|
+
```pycon exec="1" source="console" session="quickstart"
|
82
|
+
>>> cfg = hydraflow.load_config(run)
|
83
|
+
>>> print(type(cfg))
|
84
|
+
>>> print(cfg)
|
85
|
+
```
|
86
|
+
|
87
|
+
```pycon exec="1" source="console" session="quickstart"
|
88
|
+
>>> run = rc.last()
|
89
|
+
>>> cfg = hydraflow.load_config(run)
|
90
|
+
>>> print(cfg)
|
91
|
+
```
|
92
|
+
|
93
|
+
### Filter runs
|
94
|
+
|
95
|
+
```pycon exec="1" source="console" session="quickstart"
|
96
|
+
>>> filtered = rc.filter(width=400)
|
97
|
+
>>> print(filtered)
|
98
|
+
```
|
99
|
+
|
100
|
+
```pycon exec="1" source="console" session="quickstart"
|
101
|
+
>>> filtered = rc.filter(height=[100, 300])
|
102
|
+
>>> print(filtered)
|
103
|
+
```
|
104
|
+
|
105
|
+
```pycon exec="1" source="console" session="quickstart"
|
106
|
+
>>> filtered = rc.filter(height=(100, 300))
|
107
|
+
>>> print(filtered)
|
108
|
+
```
|
109
|
+
|
110
|
+
```pycon exec="1" source="console" session="quickstart"
|
111
|
+
>>> run = rc.find(height=100)
|
112
|
+
>>> print(run.data.params)
|
113
|
+
```
|
114
|
+
|
115
|
+
```pycon exec="1" source="console" session="quickstart"
|
116
|
+
>>> run = rc.find_last(height=100)
|
117
|
+
>>> print(run.data.params)
|
118
|
+
```
|
119
|
+
|
120
|
+
### Map runs
|
121
|
+
|
122
|
+
```pycon exec="1" source="console" session="quickstart"
|
123
|
+
>>> params = rc.map(lambda x: x.data.params)
|
124
|
+
>>> for p in params:
|
125
|
+
... print(p)
|
126
|
+
```
|
127
|
+
|
128
|
+
```pycon exec="1" source="console" session="quickstart"
|
129
|
+
>>> list(rc.map_id(print))
|
130
|
+
```
|
131
|
+
|
132
|
+
### Group runs
|
133
|
+
|
134
|
+
```pycon exec="1" source="console" session="quickstart"
|
135
|
+
>>> grouped = rc.groupby("width")
|
136
|
+
>>> for key, group in grouped.items():
|
137
|
+
... print(key, group)
|
138
|
+
```
|
139
|
+
|
140
|
+
```pycon exec="1" source="console" session="quickstart"
|
141
|
+
>>> grouped = rc.groupby(["height"])
|
142
|
+
>>> for key, group in grouped.items():
|
143
|
+
... print(key, group)
|
144
|
+
```
|
145
|
+
|
146
|
+
### Config dataframe
|
147
|
+
|
148
|
+
```pycon exec="1" source="console" session="quickstart"
|
149
|
+
>>> print(rc.data.config)
|
150
|
+
```
|
@@ -1,10 +1,10 @@
|
|
1
1
|
[build-system]
|
2
|
-
requires = ["hatchling
|
2
|
+
requires = ["hatchling"]
|
3
3
|
build-backend = "hatchling.build"
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "hydraflow"
|
7
|
-
version = "0.7.
|
7
|
+
version = "0.7.5"
|
8
8
|
description = "Hydraflow integrates Hydra and MLflow to manage and track machine learning experiments."
|
9
9
|
readme = "README.md"
|
10
10
|
license = { file = "LICENSE" }
|
@@ -19,40 +19,34 @@ classifiers = [
|
|
19
19
|
"Programming Language :: Python :: 3.13",
|
20
20
|
]
|
21
21
|
requires-python = ">=3.10"
|
22
|
-
dependencies = ["hydra-core>=1.3", "mlflow>=2.15"]
|
22
|
+
dependencies = ["hydra-core>=1.3", "mlflow>=2.15", "omegaconf", "rich", "typer"]
|
23
23
|
|
24
24
|
[project.urls]
|
25
25
|
Documentation = "https://daizutabi.github.io/hydraflow/"
|
26
26
|
Source = "https://github.com/daizutabi/hydraflow"
|
27
27
|
Issues = "https://github.com/daizutabi/hydraflow/issues"
|
28
28
|
|
29
|
-
[
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
"
|
29
|
+
[project.scripts]
|
30
|
+
hydraflow = "hydraflow.cli:app"
|
31
|
+
|
32
|
+
[dependency-groups]
|
33
|
+
dev = [
|
34
|
+
"pytest-clarity",
|
35
35
|
"pytest-cov",
|
36
36
|
"pytest-order",
|
37
37
|
"pytest-randomly",
|
38
38
|
"pytest-xdist",
|
39
39
|
]
|
40
|
-
|
41
|
-
[tool.hatch.build.targets.sdist]
|
42
|
-
exclude = ["/.github", "/docs"]
|
43
|
-
|
44
|
-
[tool.hatch.build.targets.wheel]
|
45
|
-
packages = ["src/hydraflow"]
|
40
|
+
docs = ["markdown-exec[ansi]", "mkapi", "mkdocs-material"]
|
46
41
|
|
47
42
|
[tool.pytest.ini_options]
|
48
43
|
addopts = [
|
49
44
|
"--cov=hydraflow",
|
50
45
|
"--cov-report=lcov:lcov.info",
|
51
|
-
"-n8",
|
52
46
|
"--dist=loadgroup",
|
47
|
+
"-n8",
|
53
48
|
]
|
54
49
|
filterwarnings = [
|
55
|
-
"ignore:pkg_resources is deprecated:DeprecationWarning",
|
56
50
|
"ignore:Support for class-based `config` is deprecated",
|
57
51
|
"ignore:Pydantic V1 style",
|
58
52
|
]
|
@@ -98,3 +92,4 @@ ignore = [
|
|
98
92
|
]
|
99
93
|
"apps/*.py" = ["D", "G", "INP"]
|
100
94
|
"src/hydraflow/main.py" = ["ANN201", "D401", "PLR0913"]
|
95
|
+
"src/hydraflow/cli.py" = ["ANN", "D"]
|
@@ -3,7 +3,13 @@
|
|
3
3
|
from hydraflow.config import select_config, select_overrides
|
4
4
|
from hydraflow.context import chdir_artifact, log_run, start_run
|
5
5
|
from hydraflow.main import main
|
6
|
-
from hydraflow.mlflow import
|
6
|
+
from hydraflow.mlflow import (
|
7
|
+
list_run_ids,
|
8
|
+
list_run_paths,
|
9
|
+
list_runs,
|
10
|
+
search_runs,
|
11
|
+
set_experiment,
|
12
|
+
)
|
7
13
|
from hydraflow.run_collection import RunCollection
|
8
14
|
from hydraflow.utils import (
|
9
15
|
get_artifact_dir,
|
@@ -22,6 +28,8 @@ __all__ = [
|
|
22
28
|
"get_artifact_path",
|
23
29
|
"get_hydra_output_dir",
|
24
30
|
"get_overrides",
|
31
|
+
"list_run_ids",
|
32
|
+
"list_run_paths",
|
25
33
|
"list_runs",
|
26
34
|
"load_config",
|
27
35
|
"load_overrides",
|
@@ -0,0 +1,75 @@
|
|
1
|
+
"""Hydraflow CLI."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import Annotated
|
7
|
+
|
8
|
+
import typer
|
9
|
+
from omegaconf import DictConfig, OmegaConf
|
10
|
+
from rich.console import Console
|
11
|
+
from typer import Argument, Option
|
12
|
+
|
13
|
+
app = typer.Typer(add_completion=False)
|
14
|
+
console = Console()
|
15
|
+
|
16
|
+
|
17
|
+
@app.command()
|
18
|
+
def run(
|
19
|
+
names: Annotated[
|
20
|
+
list[str] | None,
|
21
|
+
Argument(help="Job names.", show_default=False),
|
22
|
+
] = None,
|
23
|
+
) -> None:
|
24
|
+
"""Run jobs."""
|
25
|
+
typer.echo(names)
|
26
|
+
|
27
|
+
cfg = load_config()
|
28
|
+
typer.echo(cfg)
|
29
|
+
|
30
|
+
|
31
|
+
@app.command()
|
32
|
+
def show() -> None:
|
33
|
+
"""Show the config."""
|
34
|
+
from rich.syntax import Syntax
|
35
|
+
|
36
|
+
cfg = load_config()
|
37
|
+
code = OmegaConf.to_yaml(cfg)
|
38
|
+
syntax = Syntax(code, "yaml")
|
39
|
+
console.print(syntax)
|
40
|
+
|
41
|
+
|
42
|
+
@app.callback(invoke_without_command=True)
|
43
|
+
def callback(
|
44
|
+
*,
|
45
|
+
version: Annotated[
|
46
|
+
bool,
|
47
|
+
Option("--version", help="Show the version and exit."),
|
48
|
+
] = False,
|
49
|
+
) -> None:
|
50
|
+
if version:
|
51
|
+
import importlib.metadata
|
52
|
+
|
53
|
+
typer.echo(f"hydraflow {importlib.metadata.version('hydraflow')}")
|
54
|
+
raise typer.Exit
|
55
|
+
|
56
|
+
|
57
|
+
def find_config() -> Path:
|
58
|
+
if Path("hydraflow.yaml").exists():
|
59
|
+
return Path("hydraflow.yaml")
|
60
|
+
|
61
|
+
if Path("hydraflow.yml").exists():
|
62
|
+
return Path("hydraflow.yml")
|
63
|
+
|
64
|
+
typer.echo("No config file found.")
|
65
|
+
raise typer.Exit(code=1)
|
66
|
+
|
67
|
+
|
68
|
+
def load_config() -> DictConfig:
|
69
|
+
cfg = OmegaConf.load(find_config())
|
70
|
+
|
71
|
+
if isinstance(cfg, DictConfig):
|
72
|
+
return cfg
|
73
|
+
|
74
|
+
typer.echo("Invalid config file.")
|
75
|
+
raise typer.Exit(code=1)
|
@@ -153,18 +153,17 @@ def search_runs( # noqa: PLR0913
|
|
153
153
|
return RunCollection(runs) # type: ignore
|
154
154
|
|
155
155
|
|
156
|
-
def
|
156
|
+
def list_run_paths(
|
157
157
|
experiment_names: str | list[str] | None = None,
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
"""List all runs for the specified experiments.
|
158
|
+
*other: str,
|
159
|
+
) -> list[Path]:
|
160
|
+
"""List all run paths for the specified experiments.
|
162
161
|
|
163
|
-
This function retrieves all
|
162
|
+
This function retrieves all run paths for the given list of experiment names.
|
164
163
|
If no experiment names are provided (None), it defaults to searching all runs
|
165
164
|
for the currently active experiment. If an empty list is provided, the function
|
166
165
|
will search all runs for all experiments except the "Default" experiment.
|
167
|
-
The function returns the results as a `
|
166
|
+
The function returns the results as a list of `Path` objects.
|
168
167
|
|
169
168
|
Note:
|
170
169
|
The returned runs are sorted by their start time in ascending order.
|
@@ -174,27 +173,12 @@ def list_runs(
|
|
174
173
|
for runs. If None or an empty list is provided, the function will
|
175
174
|
search the currently active experiment or all experiments except
|
176
175
|
the "Default" experiment.
|
177
|
-
|
178
|
-
will search runs sequentially.
|
179
|
-
status (str | list[str] | int | list[int] | None): The status of the runs
|
180
|
-
to filter.
|
176
|
+
other (str): The parts of the run directory to join.
|
181
177
|
|
182
178
|
Returns:
|
183
|
-
|
184
|
-
specified experiments.
|
179
|
+
list[Path]: A list of run paths for the specified experiments.
|
185
180
|
|
186
181
|
"""
|
187
|
-
rc = _list_runs(experiment_names, n_jobs)
|
188
|
-
if status is None:
|
189
|
-
return rc
|
190
|
-
|
191
|
-
return rc.filter(status=status)
|
192
|
-
|
193
|
-
|
194
|
-
def _list_runs(
|
195
|
-
experiment_names: str | list[str] | None = None,
|
196
|
-
n_jobs: int = 0,
|
197
|
-
) -> RunCollection:
|
198
182
|
if isinstance(experiment_names, str):
|
199
183
|
experiment_names = [experiment_names]
|
200
184
|
|
@@ -202,14 +186,11 @@ def _list_runs(
|
|
202
186
|
experiments = mlflow.search_experiments()
|
203
187
|
experiment_names = [e.name for e in experiments if e.name != "Default"]
|
204
188
|
|
205
|
-
if n_jobs == 0:
|
206
|
-
return search_runs(experiment_names=experiment_names)
|
207
|
-
|
208
189
|
if experiment_names is None:
|
209
190
|
experiment_id = _get_experiment_id()
|
210
191
|
experiment_names = [mlflow.get_experiment(experiment_id).name]
|
211
192
|
|
212
|
-
|
193
|
+
run_paths: list[Path] = []
|
213
194
|
|
214
195
|
for name in experiment_names:
|
215
196
|
if experiment := mlflow.get_experiment_by_name(name):
|
@@ -217,9 +198,83 @@ def _list_runs(
|
|
217
198
|
|
218
199
|
if isinstance(uri, str):
|
219
200
|
path = get_artifact_dir(uri=uri)
|
220
|
-
|
201
|
+
run_paths.extend(p for p in path.iterdir() if p.is_dir())
|
202
|
+
|
203
|
+
if other:
|
204
|
+
return [p.joinpath(*other) for p in run_paths]
|
205
|
+
|
206
|
+
return run_paths
|
207
|
+
|
208
|
+
|
209
|
+
def list_run_ids(experiment_names: str | list[str] | None = None) -> list[str]:
|
210
|
+
"""List all run IDs for the specified experiments.
|
211
|
+
|
212
|
+
This function retrieves all runs for the given list of experiment names.
|
213
|
+
If no experiment names are provided (None), it defaults to searching all runs
|
214
|
+
for the currently active experiment. If an empty list is provided, the function
|
215
|
+
will search all runs for all experiments except the "Default" experiment.
|
216
|
+
The function returns the results as a list of string.
|
217
|
+
|
218
|
+
Note:
|
219
|
+
The returned runs are sorted by their start time in ascending order.
|
220
|
+
|
221
|
+
Args:
|
222
|
+
experiment_names (list[str] | None): List of experiment names to search
|
223
|
+
for runs. If None or an empty list is provided, the function will
|
224
|
+
search the currently active experiment or all experiments except
|
225
|
+
the "Default" experiment.
|
226
|
+
|
227
|
+
Returns:
|
228
|
+
list[str]: A list of run IDs for the specified experiments.
|
229
|
+
|
230
|
+
"""
|
231
|
+
return [run_dir.stem for run_dir in list_run_paths(experiment_names)]
|
232
|
+
|
233
|
+
|
234
|
+
def list_runs(
|
235
|
+
experiment_names: str | list[str] | None = None,
|
236
|
+
n_jobs: int = 0,
|
237
|
+
status: str | list[str] | int | list[int] | None = None,
|
238
|
+
) -> RunCollection:
|
239
|
+
"""List all runs for the specified experiments.
|
240
|
+
|
241
|
+
This function retrieves all runs for the given list of experiment names.
|
242
|
+
If no experiment names are provided (None), it defaults to searching all runs
|
243
|
+
for the currently active experiment. If an empty list is provided, the function
|
244
|
+
will search all runs for all experiments except the "Default" experiment.
|
245
|
+
The function returns the results as a `RunCollection` object.
|
246
|
+
|
247
|
+
Note:
|
248
|
+
The returned runs are sorted by their start time in ascending order.
|
249
|
+
|
250
|
+
Args:
|
251
|
+
experiment_names (list[str] | None): List of experiment names to search
|
252
|
+
for runs. If None or an empty list is provided, the function will
|
253
|
+
search the currently active experiment or all experiments except
|
254
|
+
the "Default" experiment.
|
255
|
+
n_jobs (int): The number of jobs to run in parallel. If 0, the function
|
256
|
+
will search runs sequentially.
|
257
|
+
status (str | list[str] | int | list[int] | None): The status of the runs
|
258
|
+
to filter.
|
259
|
+
|
260
|
+
Returns:
|
261
|
+
RunCollection: A `RunCollection` instance containing the runs for the
|
262
|
+
specified experiments.
|
263
|
+
|
264
|
+
"""
|
265
|
+
run_ids = list_run_ids(experiment_names)
|
266
|
+
|
267
|
+
if n_jobs == 0:
|
268
|
+
runs = [mlflow.get_run(run_id) for run_id in run_ids]
|
269
|
+
|
270
|
+
else:
|
271
|
+
it = (joblib.delayed(mlflow.get_run)(run_id) for run_id in run_ids)
|
272
|
+
runs = joblib.Parallel(n_jobs, prefer="threads")(it)
|
221
273
|
|
222
|
-
it = (joblib.delayed(mlflow.get_run)(run_id) for run_id in run_ids)
|
223
|
-
runs = joblib.Parallel(n_jobs, prefer="threads")(it)
|
224
274
|
runs = sorted(runs, key=lambda run: run.info.start_time) # type: ignore
|
225
|
-
|
275
|
+
rc = RunCollection(runs) # type: ignore
|
276
|
+
|
277
|
+
if status is None:
|
278
|
+
return rc
|
279
|
+
|
280
|
+
return rc.filter(status=status)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
from typer.testing import CliRunner
|
4
|
+
|
5
|
+
from hydraflow.cli import app
|
6
|
+
|
7
|
+
runner = CliRunner()
|
8
|
+
|
9
|
+
|
10
|
+
def test_invoke_error():
|
11
|
+
result = runner.invoke(app, ["run"])
|
12
|
+
assert result.exit_code == 1
|
13
|
+
|
14
|
+
|
15
|
+
def test_invoke():
|
16
|
+
Path("hydraflow.yaml").write_text("a:\n b: [1, 2]")
|
17
|
+
result = runner.invoke(app, ["run"])
|
18
|
+
assert result.exit_code == 0
|
@@ -0,0 +1,52 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
import pytest
|
4
|
+
import typer
|
5
|
+
from typer.testing import CliRunner
|
6
|
+
|
7
|
+
from hydraflow.cli import app
|
8
|
+
|
9
|
+
runner = CliRunner()
|
10
|
+
|
11
|
+
|
12
|
+
@pytest.mark.parametrize("file", ["hydraflow.yaml", "hydraflow.yml"])
|
13
|
+
def test_find_config(file):
|
14
|
+
from hydraflow.cli import find_config
|
15
|
+
|
16
|
+
Path(file).touch()
|
17
|
+
assert find_config() == Path(file)
|
18
|
+
|
19
|
+
|
20
|
+
def test_find_config_error():
|
21
|
+
from hydraflow.cli import find_config
|
22
|
+
|
23
|
+
with pytest.raises(typer.Exit):
|
24
|
+
find_config()
|
25
|
+
|
26
|
+
|
27
|
+
def test_load_config():
|
28
|
+
from hydraflow.cli import load_config
|
29
|
+
|
30
|
+
Path("hydraflow.yaml").write_text("a:\n b: 1")
|
31
|
+
cfg = load_config()
|
32
|
+
assert cfg["a"]["b"] == 1
|
33
|
+
|
34
|
+
|
35
|
+
def test_load_config_error():
|
36
|
+
from hydraflow.cli import load_config
|
37
|
+
|
38
|
+
Path("hydraflow.yml").write_text("- 1\n- 2")
|
39
|
+
|
40
|
+
with pytest.raises(typer.Exit):
|
41
|
+
load_config()
|
42
|
+
|
43
|
+
|
44
|
+
def test_invoke_error():
|
45
|
+
result = runner.invoke(app, ["show"])
|
46
|
+
assert result.exit_code == 1
|
47
|
+
|
48
|
+
|
49
|
+
def test_invoke():
|
50
|
+
Path("hydraflow.yaml").write_text("a:\n b: [1, 2]")
|
51
|
+
result = runner.invoke(app, ["show"])
|
52
|
+
assert result.exit_code == 0
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import subprocess
|
2
|
+
|
3
|
+
from typer.testing import CliRunner
|
4
|
+
|
5
|
+
from hydraflow.cli import app
|
6
|
+
|
7
|
+
runner = CliRunner()
|
8
|
+
|
9
|
+
|
10
|
+
def test_invoke():
|
11
|
+
result = runner.invoke(app, ["--version"])
|
12
|
+
assert result.exit_code == 0
|
13
|
+
assert "hydraflow" in result.stdout
|
14
|
+
assert result.stdout.count(".") == 2
|
15
|
+
|
16
|
+
|
17
|
+
def test_command():
|
18
|
+
out = subprocess.check_output(["hydraflow", "--version"], text=True)
|
19
|
+
assert "hydraflow" in out
|
20
|
+
assert out.count(".") == 2
|
@@ -92,3 +92,10 @@ def test_list_runs(experiment: Experiment, status, n, n_jobs, func):
|
|
92
92
|
experiment_names = func(experiment.name)
|
93
93
|
rc = list_runs(experiment_names=experiment_names, status=status, n_jobs=n_jobs)
|
94
94
|
assert len(rc) == n
|
95
|
+
|
96
|
+
|
97
|
+
def test_list_run_dirs(experiment: Experiment):
|
98
|
+
from hydraflow.mlflow import list_run_paths
|
99
|
+
|
100
|
+
dirs = list_run_paths(experiment.name, "artifacts")
|
101
|
+
assert all(d.is_dir() for d in dirs)
|
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
|