hydraflow 0.12.3__tar.gz → 0.12.5__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.
- {hydraflow-0.12.3 → hydraflow-0.12.5}/.github/workflows/ci.yaml +1 -2
- {hydraflow-0.12.3 → hydraflow-0.12.5}/.github/workflows/docs.yaml +14 -10
- hydraflow-0.12.5/.github/workflows/publish.yaml +37 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/PKG-INFO +1 -1
- {hydraflow-0.12.3 → hydraflow-0.12.5}/docs/index.md +9 -9
- {hydraflow-0.12.3 → hydraflow-0.12.5}/pyproject.toml +1 -1
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/cli.py +26 -2
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/core/io.py +37 -33
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/executor/conf.py +1 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/executor/job.py +73 -48
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/cli/hydraflow.yaml +18 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/cli/test_run.py +30 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/io/test_iter_dirs.py +32 -16
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/test_mlflow.py +0 -7
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/executor/test_job.py +33 -33
- {hydraflow-0.12.3 → hydraflow-0.12.5}/.devcontainer/devcontainer.json +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/.devcontainer/postCreate.sh +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/.devcontainer/starship.toml +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/.gitattributes +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/.gitignore +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/LICENSE +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/README.md +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/apps/quickstart.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/docs/usage/quickstart.md +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/mkdocs.yaml +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/__init__.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/core/__init__.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/core/config.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/core/context.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/core/main.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/core/mlflow.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/core/param.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/entities/__init__.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/entities/run_collection.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/entities/run_data.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/entities/run_info.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/executor/__init__.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/executor/io.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/executor/parser.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/src/hydraflow/py.typed +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/__init__.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/cli/__init__.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/cli/app.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/cli/conftest.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/cli/test_setup.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/cli/test_show.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/cli/test_version.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/conftest.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/__init__.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/config/__init__.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/config/test_config.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/config/test_params.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/context/__init__.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/context/chdir.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/context/log_run.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/context/start_run.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/context/test_chdir.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/context/test_log_run.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/context/test_start_run.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/io/__init__.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/io/hydra_dir.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/io/test_hydra_dir.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/io/test_run.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/main/__init__.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/main/default.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/main/force_new_run.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/main/match_overrides.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/main/rerun_finished.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/main/skip_finished.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/main/test_default.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/main/test_force_new_run.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/main/test_match_overrides.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/main/test_rerun_finished.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/main/test_skip_finished.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/param/__init__.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/param/params.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/param/test_param.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/core/param/test_params.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/entities/__init__.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/entities/filter.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/entities/test_collection.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/entities/test_data.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/entities/test_filter.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/entities/test_info.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/entities/test_values.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/entities/values.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/executor/__init__.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/executor/conftest.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/executor/echo.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/executor/test_args.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/executor/test_conf.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/executor/test_io.py +0 -0
- {hydraflow-0.12.3 → hydraflow-0.12.5}/tests/executor/test_parser.py +0 -0
@@ -14,7 +14,7 @@ env:
|
|
14
14
|
FORCE_COLOR: "1"
|
15
15
|
|
16
16
|
jobs:
|
17
|
-
|
17
|
+
ci:
|
18
18
|
name: Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }}
|
19
19
|
runs-on: ${{ matrix.os }}
|
20
20
|
strategy:
|
@@ -29,7 +29,6 @@ jobs:
|
|
29
29
|
uses: actions/setup-python@v5
|
30
30
|
with:
|
31
31
|
python-version: ${{ matrix.python-version }}
|
32
|
-
allow-prereleases: true
|
33
32
|
- name: Install uv and ruff
|
34
33
|
run: pip install uv ruff
|
35
34
|
- name: Install the project
|
@@ -1,25 +1,29 @@
|
|
1
1
|
name: Documentation
|
2
|
+
|
2
3
|
on:
|
3
4
|
push:
|
4
5
|
branches: [main]
|
5
|
-
tags:
|
6
|
-
|
7
|
-
|
6
|
+
tags:
|
7
|
+
- "[0-9]+.[0-9]+.[0-9]+"
|
8
|
+
|
8
9
|
jobs:
|
9
|
-
|
10
|
-
name: Documentation
|
10
|
+
docs:
|
11
11
|
runs-on: ubuntu-latest
|
12
|
+
permissions:
|
13
|
+
contents: write
|
12
14
|
steps:
|
13
15
|
- uses: actions/checkout@v4
|
14
16
|
- name: Configure Git Credentials
|
15
17
|
run: |
|
16
18
|
git config user.name github-actions[bot]
|
17
19
|
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
18
|
-
- name: Set up Python 3.
|
20
|
+
- name: Set up Python 3.13
|
19
21
|
uses: actions/setup-python@v5
|
20
22
|
with:
|
21
|
-
python-version: 3.
|
22
|
-
- name: Install
|
23
|
-
run: pip install
|
23
|
+
python-version: 3.13
|
24
|
+
- name: Install uv
|
25
|
+
run: pip install uv
|
26
|
+
- name: Install the project
|
27
|
+
run: uv sync --group docs
|
24
28
|
- name: Deploy documentation
|
25
|
-
run: mkdocs gh-deploy --force
|
29
|
+
run: uv run mkdocs gh-deploy --force
|
@@ -0,0 +1,37 @@
|
|
1
|
+
name: Publish
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
tags:
|
6
|
+
- "[0-9]+.[0-9]+.[0-9]+"
|
7
|
+
|
8
|
+
jobs:
|
9
|
+
publish:
|
10
|
+
runs-on: ubuntu-latest
|
11
|
+
permissions:
|
12
|
+
id-token: write
|
13
|
+
steps:
|
14
|
+
- uses: actions/checkout@v4
|
15
|
+
|
16
|
+
- name: Verify version match
|
17
|
+
run: |
|
18
|
+
TAG=${GITHUB_REF#refs/tags/}
|
19
|
+
PYPROJECT_VERSION=$(grep -m 1 "version = " pyproject.toml | cut -d'"' -f2)
|
20
|
+
if [ "$TAG" != "$PYPROJECT_VERSION" ]; then
|
21
|
+
echo "Error: Git tag ($TAG) does not match pyproject.toml version ($PYPROJECT_VERSION)"
|
22
|
+
exit 1
|
23
|
+
fi
|
24
|
+
echo "Version verified: $TAG"
|
25
|
+
|
26
|
+
- name: Set up Python
|
27
|
+
uses: actions/setup-python@v5
|
28
|
+
with:
|
29
|
+
python-version: 3.13
|
30
|
+
- name: Install uv
|
31
|
+
run: pip install uv
|
32
|
+
- name: Install the project
|
33
|
+
run: uv sync
|
34
|
+
- name: Build the project
|
35
|
+
run: uv build --no-sources
|
36
|
+
- name: Upload the project to PyPI
|
37
|
+
run: uv publish
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hydraflow
|
3
|
-
Version: 0.12.
|
3
|
+
Version: 0.12.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
|
@@ -1,10 +1,10 @@
|
|
1
|
-
# Hydraflow Documentation
|
2
|
-
|
3
|
-
Hydraflow integrates [Hydra](https://hydra.cc/) and [MLflow](https://mlflow.org/)
|
4
|
-
to manage and track machine learning experiments.
|
5
|
-
|
6
|
-
## Installation
|
7
|
-
|
8
|
-
```bash
|
9
|
-
pip install hydraflow
|
1
|
+
# Hydraflow Documentation
|
2
|
+
|
3
|
+
Hydraflow integrates [Hydra](https://hydra.cc/) and [MLflow](https://mlflow.org/)
|
4
|
+
to manage and track machine learning experiments.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
```bash
|
9
|
+
pip install hydraflow
|
10
10
|
```
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "hydraflow"
|
7
|
-
version = "0.12.
|
7
|
+
version = "0.12.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" }
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
+
import shlex
|
5
6
|
from typing import Annotated
|
6
7
|
|
7
8
|
import typer
|
@@ -24,7 +25,13 @@ def run(
|
|
24
25
|
"""Run a job."""
|
25
26
|
|
26
27
|
from hydraflow.executor.io import get_job
|
27
|
-
from hydraflow.executor.job import
|
28
|
+
from hydraflow.executor.job import (
|
29
|
+
iter_batches,
|
30
|
+
iter_calls,
|
31
|
+
iter_runs,
|
32
|
+
submit,
|
33
|
+
to_text,
|
34
|
+
)
|
28
35
|
|
29
36
|
job = get_job(name)
|
30
37
|
|
@@ -35,7 +42,24 @@ def run(
|
|
35
42
|
import mlflow
|
36
43
|
|
37
44
|
mlflow.set_experiment(job.name)
|
38
|
-
|
45
|
+
|
46
|
+
if job.submit:
|
47
|
+
funcname, *args = shlex.split(job.submit)
|
48
|
+
submit(funcname, args, iter_batches(job))
|
49
|
+
raise typer.Exit
|
50
|
+
|
51
|
+
if job.run:
|
52
|
+
executable, *args = shlex.split(job.run)
|
53
|
+
it = iter_runs(executable, args, iter_batches(job))
|
54
|
+
elif job.call:
|
55
|
+
funcname, *args = shlex.split(job.call)
|
56
|
+
it = iter_calls(funcname, args, iter_batches(job))
|
57
|
+
else:
|
58
|
+
typer.echo(f"No command found in job: {job.name}.")
|
59
|
+
raise typer.Exit(1)
|
60
|
+
|
61
|
+
for _ in it:
|
62
|
+
pass
|
39
63
|
|
40
64
|
|
41
65
|
@app.command()
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
+
import fnmatch
|
5
6
|
import shutil
|
6
7
|
import urllib.parse
|
7
8
|
import urllib.request
|
@@ -12,7 +13,7 @@ from hydra.core.hydra_config import HydraConfig
|
|
12
13
|
from omegaconf import DictConfig, ListConfig, OmegaConf
|
13
14
|
|
14
15
|
if TYPE_CHECKING:
|
15
|
-
from collections.abc import Iterable, Iterator
|
16
|
+
from collections.abc import Callable, Iterable, Iterator
|
16
17
|
|
17
18
|
from mlflow.entities import Run
|
18
19
|
|
@@ -152,21 +153,6 @@ def remove_run(run: Run | Iterable[Run]) -> None:
|
|
152
153
|
shutil.rmtree(get_artifact_dir(run).parent)
|
153
154
|
|
154
155
|
|
155
|
-
def get_root_dir(uri: str | Path | None = None) -> Path:
|
156
|
-
"""Get the root directory for the MLflow tracking server."""
|
157
|
-
import mlflow
|
158
|
-
|
159
|
-
if uri is not None:
|
160
|
-
return Path(uri).absolute()
|
161
|
-
|
162
|
-
uri = mlflow.get_tracking_uri()
|
163
|
-
|
164
|
-
if uri.startswith("file:"):
|
165
|
-
return file_uri_to_path(uri)
|
166
|
-
|
167
|
-
return Path(uri).absolute()
|
168
|
-
|
169
|
-
|
170
156
|
def get_experiment_name(path: Path) -> str | None:
|
171
157
|
"""Get the experiment name from the meta file."""
|
172
158
|
metafile = path / "meta.yaml"
|
@@ -179,47 +165,65 @@ def get_experiment_name(path: Path) -> str | None:
|
|
179
165
|
return None
|
180
166
|
|
181
167
|
|
168
|
+
def predicate_experiment_dir(
|
169
|
+
path: Path,
|
170
|
+
experiment_names: list[str] | Callable[[str], bool] | None = None,
|
171
|
+
) -> bool:
|
172
|
+
"""Predicate an experiment directory based on the path and experiment names."""
|
173
|
+
if not path.is_dir() or path.name in [".trash", "0"]:
|
174
|
+
return False
|
175
|
+
|
176
|
+
name = get_experiment_name(path)
|
177
|
+
if not name:
|
178
|
+
return False
|
179
|
+
|
180
|
+
if experiment_names is None:
|
181
|
+
return True
|
182
|
+
|
183
|
+
if isinstance(experiment_names, list):
|
184
|
+
return any(fnmatch.fnmatch(name, e) for e in experiment_names)
|
185
|
+
|
186
|
+
return experiment_names(name)
|
187
|
+
|
188
|
+
|
182
189
|
def iter_experiment_dirs(
|
183
|
-
|
184
|
-
|
190
|
+
root_dir: str | Path,
|
191
|
+
experiment_names: str | list[str] | Callable[[str], bool] | None = None,
|
185
192
|
) -> Iterator[Path]:
|
186
193
|
"""Iterate over the experiment directories in the root directory."""
|
187
194
|
if isinstance(experiment_names, str):
|
188
195
|
experiment_names = [experiment_names]
|
189
196
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
if name := get_experiment_name(path):
|
194
|
-
if experiment_names is None or name in experiment_names:
|
195
|
-
yield path
|
197
|
+
for path in Path(root_dir).iterdir():
|
198
|
+
if predicate_experiment_dir(path, experiment_names):
|
199
|
+
yield path
|
196
200
|
|
197
201
|
|
198
202
|
def iter_run_dirs(
|
199
|
-
|
200
|
-
|
203
|
+
root_dir: str | Path,
|
204
|
+
experiment_names: str | list[str] | Callable[[str], bool] | None = None,
|
201
205
|
) -> Iterator[Path]:
|
202
206
|
"""Iterate over the run directories in the root directory."""
|
203
|
-
for experiment_dir in iter_experiment_dirs(
|
207
|
+
for experiment_dir in iter_experiment_dirs(root_dir, experiment_names):
|
204
208
|
for path in experiment_dir.iterdir():
|
205
209
|
if path.is_dir() and (path / "artifacts").exists():
|
206
210
|
yield path
|
207
211
|
|
208
212
|
|
209
213
|
def iter_artifacts_dirs(
|
210
|
-
|
211
|
-
|
214
|
+
root_dir: str | Path,
|
215
|
+
experiment_names: str | list[str] | Callable[[str], bool] | None = None,
|
212
216
|
) -> Iterator[Path]:
|
213
217
|
"""Iterate over the artifacts directories in the root directory."""
|
214
|
-
for path in iter_run_dirs(
|
218
|
+
for path in iter_run_dirs(root_dir, experiment_names):
|
215
219
|
yield path / "artifacts"
|
216
220
|
|
217
221
|
|
218
222
|
def iter_artifact_paths(
|
223
|
+
root_dir: str | Path,
|
219
224
|
artifact_path: str | Path,
|
220
|
-
experiment_names: str | list[str] | None = None,
|
221
|
-
root_dir: str | Path | None = None,
|
225
|
+
experiment_names: str | list[str] | Callable[[str], bool] | None = None,
|
222
226
|
) -> Iterator[Path]:
|
223
227
|
"""Iterate over the artifact paths in the root directory."""
|
224
|
-
for path in iter_artifacts_dirs(
|
228
|
+
for path in iter_artifacts_dirs(root_dir, experiment_names):
|
225
229
|
yield path / artifact_path
|
@@ -21,7 +21,7 @@ import importlib
|
|
21
21
|
import shlex
|
22
22
|
import subprocess
|
23
23
|
import sys
|
24
|
-
from
|
24
|
+
from dataclasses import dataclass
|
25
25
|
from typing import TYPE_CHECKING
|
26
26
|
|
27
27
|
import ulid
|
@@ -29,7 +29,9 @@ import ulid
|
|
29
29
|
from .parser import collect, expand
|
30
30
|
|
31
31
|
if TYPE_CHECKING:
|
32
|
-
from collections.abc import Iterator
|
32
|
+
from collections.abc import Callable, Iterable, Iterator
|
33
|
+
from subprocess import CompletedProcess
|
34
|
+
from typing import Any
|
33
35
|
|
34
36
|
from .conf import Job
|
35
37
|
|
@@ -79,63 +81,81 @@ def iter_batches(job: Job) -> Iterator[list[str]]:
|
|
79
81
|
yield ["--multirun", *args, job_name, sweep_dir, *configs]
|
80
82
|
|
81
83
|
|
82
|
-
|
83
|
-
|
84
|
+
@dataclass
|
85
|
+
class Run:
|
86
|
+
"""An executed run."""
|
84
87
|
|
85
|
-
|
88
|
+
total: int
|
89
|
+
completed: int
|
90
|
+
result: CompletedProcess
|
86
91
|
|
87
|
-
1. Shell command mode (job.run): Executes shell commands with the generated
|
88
|
-
arguments
|
89
|
-
2. Python function mode (job.call): Calls a Python function with the generated
|
90
|
-
arguments
|
91
92
|
|
92
|
-
|
93
|
-
|
93
|
+
@dataclass
|
94
|
+
class Call:
|
95
|
+
"""An executed call."""
|
94
96
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
ValueError: If the Python function path is invalid or the function cannot
|
99
|
-
be imported.
|
97
|
+
total: int
|
98
|
+
completed: int
|
99
|
+
result: Any
|
100
100
|
|
101
|
-
"""
|
102
|
-
it = iter_batches(job)
|
103
101
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
102
|
+
def iter_runs(
|
103
|
+
executable: str,
|
104
|
+
args: list[str],
|
105
|
+
iterable: Iterable[list[str]],
|
106
|
+
) -> Iterator[Run]:
|
107
|
+
"""Execute multiple runs of a job using shell commands."""
|
108
|
+
if executable == "python" and sys.platform == "win32":
|
109
|
+
executable = sys.executable
|
108
110
|
|
109
|
-
|
110
|
-
|
111
|
-
try:
|
112
|
-
subprocess.run(cmds, check=True)
|
113
|
-
except CalledProcessError as e:
|
114
|
-
msg = f"Command failed with exit code {e.returncode}"
|
115
|
-
raise RuntimeError(msg) from e
|
111
|
+
iterable = list(iterable)
|
112
|
+
total = len(iterable)
|
116
113
|
|
117
|
-
|
118
|
-
|
114
|
+
for completed, args_ in enumerate(iterable, 1):
|
115
|
+
result = subprocess.run([executable, *args, *args_], check=False)
|
116
|
+
yield Run(total, completed, result)
|
119
117
|
|
120
|
-
if "." not in call_name:
|
121
|
-
msg = f"Invalid function path: {call_name}."
|
122
|
-
msg += " Expected format: 'package.module.function'"
|
123
|
-
raise ValueError(msg)
|
124
118
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
119
|
+
def iter_calls(
|
120
|
+
funcname: str,
|
121
|
+
args: list[str],
|
122
|
+
iterable: Iterable[list[str]],
|
123
|
+
) -> Iterator[Call]:
|
124
|
+
"""Execute multiple calls of a job using Python functions."""
|
125
|
+
func = get_callable(funcname)
|
132
126
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
127
|
+
iterable = list(iterable)
|
128
|
+
total = len(iterable)
|
129
|
+
|
130
|
+
for completed, args_ in enumerate(iterable, 1):
|
131
|
+
result = func([*args, *args_])
|
132
|
+
yield Call(total, completed, result)
|
133
|
+
|
134
|
+
|
135
|
+
def submit(
|
136
|
+
funcname: str,
|
137
|
+
args: list[str],
|
138
|
+
iterable: Iterable[list[str]],
|
139
|
+
) -> Any:
|
140
|
+
"""Submit entire job using Python functions."""
|
141
|
+
func = get_callable(funcname)
|
142
|
+
return func([[*args, *a] for a in iterable])
|
143
|
+
|
144
|
+
|
145
|
+
def get_callable(name: str) -> Callable:
|
146
|
+
"""Get a callable from a function name."""
|
147
|
+
if "." not in name:
|
148
|
+
msg = f"Invalid function path: {name}."
|
149
|
+
raise ValueError(msg)
|
150
|
+
|
151
|
+
try:
|
152
|
+
module_name, func_name = name.rsplit(".", 1)
|
153
|
+
module = importlib.import_module(module_name)
|
154
|
+
return getattr(module, func_name)
|
155
|
+
|
156
|
+
except (ImportError, AttributeError, ModuleNotFoundError) as e:
|
157
|
+
msg = f"Failed to import or find function: {name}"
|
158
|
+
raise ValueError(msg) from e
|
139
159
|
|
140
160
|
|
141
161
|
def to_text(job: Job) -> str:
|
@@ -165,4 +185,9 @@ def to_text(job: Job) -> str:
|
|
165
185
|
for args in it:
|
166
186
|
text += f"args: {args}\n"
|
167
187
|
|
188
|
+
elif job.submit:
|
189
|
+
text = f"submit: {job.submit}\n"
|
190
|
+
for args in it:
|
191
|
+
text += f"args: {args}\n"
|
192
|
+
|
168
193
|
return text.rstrip()
|
@@ -20,3 +20,21 @@ jobs:
|
|
20
20
|
- batch: name=b
|
21
21
|
args: count=11:14
|
22
22
|
with: hydra/launcher=joblib hydra.launcher.n_jobs=4
|
23
|
+
echo:
|
24
|
+
call: typer.echo a b c
|
25
|
+
steps:
|
26
|
+
- batch: name=a,b
|
27
|
+
args: count=1:3
|
28
|
+
- batch: name=c,d
|
29
|
+
args: count=4:6
|
30
|
+
submit:
|
31
|
+
submit: typer.echo
|
32
|
+
steps:
|
33
|
+
- batch: name=a
|
34
|
+
args: count=1,3
|
35
|
+
- batch: name=b
|
36
|
+
args: count=5,7
|
37
|
+
error:
|
38
|
+
steps:
|
39
|
+
- batch: name=a
|
40
|
+
args: count=1:3
|
@@ -61,3 +61,33 @@ def test_run_parallel():
|
|
61
61
|
assert result.exit_code == 0
|
62
62
|
run_ids = hydraflow.list_run_ids("parallel")
|
63
63
|
assert len(run_ids) == 8
|
64
|
+
|
65
|
+
|
66
|
+
@pytest.mark.xdist_group(name="group4")
|
67
|
+
def test_run_echo():
|
68
|
+
result = runner.invoke(app, ["run", "echo"])
|
69
|
+
assert result.exit_code == 0
|
70
|
+
out = result.stdout
|
71
|
+
lines = out.splitlines()
|
72
|
+
assert len(lines) == 5
|
73
|
+
assert "['a', 'b', 'c', '--multirun', 'name=a', 'count=1,2,3'" in lines[1]
|
74
|
+
assert "['a', 'b', 'c', '--multirun', 'name=b', 'count=1,2,3'" in lines[2]
|
75
|
+
assert "['a', 'b', 'c', '--multirun', 'name=c', 'count=4,5,6'" in lines[3]
|
76
|
+
assert "['a', 'b', 'c', '--multirun', 'name=d', 'count=4,5,6'" in lines[4]
|
77
|
+
|
78
|
+
|
79
|
+
@pytest.mark.xdist_group(name="group4")
|
80
|
+
def test_run_submit():
|
81
|
+
result = runner.invoke(app, ["run", "submit"])
|
82
|
+
assert result.exit_code == 0
|
83
|
+
out = result.stdout
|
84
|
+
lines = out.splitlines()
|
85
|
+
assert len(lines) == 2
|
86
|
+
assert "[['--multirun', 'name=a', 'count=1,3', 'hydra.job.name=submit'" in lines[1]
|
87
|
+
|
88
|
+
|
89
|
+
@pytest.mark.xdist_group(name="group5")
|
90
|
+
def test_run_error():
|
91
|
+
result = runner.invoke(app, ["run", "error"])
|
92
|
+
assert result.exit_code == 1
|
93
|
+
assert "No command found in job: error." in result.stdout
|
@@ -27,28 +27,44 @@ def setup(chdir):
|
|
27
27
|
mlflow.log_text("5", "text.txt")
|
28
28
|
|
29
29
|
|
30
|
-
def
|
31
|
-
from hydraflow.core.io import
|
30
|
+
def test_iter_experiment_dirs(root_dir: Path):
|
31
|
+
from hydraflow.core.io import get_experiment_name, iter_experiment_dirs
|
32
32
|
|
33
|
-
|
34
|
-
assert
|
35
|
-
assert get_root_dir() == root_dir
|
33
|
+
names = [get_experiment_name(p) for p in iter_experiment_dirs(root_dir)]
|
34
|
+
assert sorted(names) == ["e1", "e2"] # type: ignore
|
36
35
|
|
37
36
|
|
38
|
-
|
37
|
+
@pytest.mark.parametrize(
|
38
|
+
("e", "es"),
|
39
|
+
[("e1", ["e1"]), ("e*", ["e1", "e2"]), ("*", ["e1", "e2"]), ("*2", ["e2"])],
|
40
|
+
)
|
41
|
+
def test_iter_experiment_dirs_glob(root_dir: Path, e, es):
|
39
42
|
from hydraflow.core.io import get_experiment_name, iter_experiment_dirs
|
40
43
|
|
41
|
-
names = [get_experiment_name(p) for p in iter_experiment_dirs()]
|
42
|
-
assert sorted(names) ==
|
44
|
+
names = [get_experiment_name(p) for p in iter_experiment_dirs(root_dir, e)]
|
45
|
+
assert sorted(names) == es # type: ignore
|
43
46
|
|
44
47
|
|
45
|
-
def test_iter_experiment_dirs_filter():
|
48
|
+
def test_iter_experiment_dirs_filter(root_dir: Path):
|
46
49
|
from hydraflow.core.io import get_experiment_name, iter_experiment_dirs
|
47
50
|
|
48
|
-
it = iter_experiment_dirs(experiment_names="e1")
|
51
|
+
it = iter_experiment_dirs(root_dir, experiment_names="e1")
|
49
52
|
assert [get_experiment_name(p) for p in it] == ["e1"]
|
50
53
|
|
51
54
|
|
55
|
+
def test_iter_experiment_dirs_filter_callable(root_dir: Path):
|
56
|
+
from hydraflow.core.io import get_experiment_name, iter_experiment_dirs
|
57
|
+
|
58
|
+
it = iter_experiment_dirs(root_dir, experiment_names=lambda name: name == "e2")
|
59
|
+
assert [get_experiment_name(p) for p in it] == ["e2"]
|
60
|
+
|
61
|
+
|
62
|
+
def test_predicate_experiment_dir():
|
63
|
+
from hydraflow.core.io import predicate_experiment_dir
|
64
|
+
|
65
|
+
assert predicate_experiment_dir(Path()) is False
|
66
|
+
|
67
|
+
|
52
68
|
def test_get_experiment_name_none(root_dir: Path):
|
53
69
|
from hydraflow.core.io import get_experiment_name
|
54
70
|
|
@@ -62,21 +78,21 @@ def test_get_experiment_name_metafile_none(root_dir: Path):
|
|
62
78
|
assert get_experiment_name(root_dir) is None
|
63
79
|
|
64
80
|
|
65
|
-
def test_iter_run_dirs():
|
81
|
+
def test_iter_run_dirs(root_dir: Path):
|
66
82
|
from hydraflow.core.io import iter_run_dirs
|
67
83
|
|
68
|
-
assert len(list(iter_run_dirs())) == 5
|
84
|
+
assert len(list(iter_run_dirs(root_dir))) == 5
|
69
85
|
|
70
86
|
|
71
|
-
def test_iter_artifacts_dirs():
|
87
|
+
def test_iter_artifacts_dirs(root_dir: Path):
|
72
88
|
from hydraflow.core.io import iter_artifacts_dirs
|
73
89
|
|
74
|
-
assert len(list(iter_artifacts_dirs())) == 5
|
90
|
+
assert len(list(iter_artifacts_dirs(root_dir))) == 5
|
75
91
|
|
76
92
|
|
77
|
-
def test_iter_artifact_paths():
|
93
|
+
def test_iter_artifact_paths(root_dir: Path):
|
78
94
|
from hydraflow.core.io import iter_artifact_paths
|
79
95
|
|
80
|
-
it = iter_artifact_paths("text.txt")
|
96
|
+
it = iter_artifact_paths(root_dir, "text.txt")
|
81
97
|
text = sorted("".join(p.read_text() for p in it))
|
82
98
|
assert text == ["1", "2", "3", "4", "5"]
|
@@ -81,10 +81,3 @@ def test_list_run_paths(experiment: Experiment):
|
|
81
81
|
|
82
82
|
dirs = list_run_paths(experiment.name, "artifacts")
|
83
83
|
assert all(d.is_dir() for d in dirs)
|
84
|
-
|
85
|
-
|
86
|
-
@pytest.mark.parametrize("uri", [None, "test_mlflow"])
|
87
|
-
def test_root_dir(experiment, uri):
|
88
|
-
from hydraflow.core.io import get_root_dir
|
89
|
-
|
90
|
-
assert get_root_dir(uri) == Path.cwd() / "test_mlflow"
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import sys
|
2
1
|
from pathlib import Path
|
3
2
|
|
4
3
|
import pytest
|
@@ -60,66 +59,67 @@ def test_sweep_args(batches, i, x):
|
|
60
59
|
assert batches[i][-3] == x
|
61
60
|
|
62
61
|
|
63
|
-
def
|
64
|
-
from hydraflow.executor.job import
|
62
|
+
def test_iter_runs(job: Job, tmp_path: Path):
|
63
|
+
from hydraflow.executor.job import iter_batches, iter_runs
|
65
64
|
|
66
65
|
path = tmp_path / "output.txt"
|
67
66
|
file = Path(__file__).parent / "echo.py"
|
68
67
|
|
69
|
-
|
70
|
-
|
68
|
+
args = [file.as_posix(), path.as_posix()]
|
69
|
+
x = list(iter_runs("python", args, iter_batches(job)))
|
71
70
|
assert path.read_text() == "b=5 a=1,2 b=6 a=1,2 c=7 a=3,4 c=8 a=3,4"
|
71
|
+
assert x[0].completed == 1
|
72
|
+
assert x[0].result.returncode == 0
|
73
|
+
assert x[1].completed == 2
|
74
|
+
assert x[1].result.returncode == 0
|
75
|
+
assert x[2].completed == 3
|
76
|
+
assert x[2].result.returncode == 0
|
72
77
|
|
73
78
|
|
74
|
-
def
|
75
|
-
from hydraflow.executor.job import
|
79
|
+
def test_iter_calls(job: Job, capsys: pytest.CaptureFixture):
|
80
|
+
from hydraflow.executor.job import iter_batches, iter_calls
|
76
81
|
|
77
|
-
|
78
|
-
with pytest.raises(RuntimeError):
|
79
|
-
multirun(job)
|
80
|
-
|
81
|
-
|
82
|
-
def test_multirun_call(job: Job, capsys: pytest.CaptureFixture):
|
83
|
-
from hydraflow.executor.job import multirun
|
84
|
-
|
85
|
-
job.call = "typer.echo"
|
86
|
-
multirun(job)
|
82
|
+
x = list(iter_calls("typer.echo", [], iter_batches(job)))
|
87
83
|
out, _ = capsys.readouterr()
|
88
84
|
assert "'b=5', 'a=1,2'" in out
|
89
85
|
assert "'c=8', 'a=3,4'" in out
|
86
|
+
assert x[0].completed == 1
|
87
|
+
assert x[1].completed == 2
|
88
|
+
assert x[2].completed == 3
|
90
89
|
|
91
90
|
|
92
|
-
def
|
93
|
-
from hydraflow.executor.job import
|
91
|
+
def test_iter_calls_args(job: Job, capsys: pytest.CaptureFixture):
|
92
|
+
from hydraflow.executor.job import iter_batches, iter_calls
|
94
93
|
|
95
94
|
job.call = "typer.echo a 'b c'"
|
96
|
-
|
95
|
+
list(iter_calls("typer.echo", ["a", "b c"], iter_batches(job)))
|
97
96
|
out, _ = capsys.readouterr()
|
98
97
|
assert "['a', 'b c', '--multirun'," in out
|
99
98
|
|
100
99
|
|
101
|
-
def
|
102
|
-
from hydraflow.executor.job import
|
100
|
+
def test_submit(job: Job, capsys: pytest.CaptureFixture):
|
101
|
+
from hydraflow.executor.job import iter_batches, submit
|
103
102
|
|
104
|
-
|
105
|
-
|
106
|
-
|
103
|
+
submit("typer.echo", ["a"], iter_batches(job))
|
104
|
+
out, _ = capsys.readouterr()
|
105
|
+
assert out.startswith("[['a', '--multirun', 'b=5', 'a=1,2', 'hydra.job.name=test'")
|
106
|
+
assert "], ['a', '--multirun', 'b=6', 'a=1,2', 'hydra" in out
|
107
|
+
assert "], ['a', '--multirun', 'c=7', 'a=3,4', 'hydra" in out
|
108
|
+
assert "], ['a', '--multirun', 'c=8', 'a=3,4', 'hydra" in out
|
107
109
|
|
108
110
|
|
109
|
-
def
|
110
|
-
from hydraflow.executor.job import
|
111
|
+
def test_get_callable_error():
|
112
|
+
from hydraflow.executor.job import get_callable
|
111
113
|
|
112
|
-
job.call = "print"
|
113
114
|
with pytest.raises(ValueError):
|
114
|
-
|
115
|
+
get_callable("print")
|
115
116
|
|
116
117
|
|
117
|
-
def
|
118
|
-
from hydraflow.executor.job import
|
118
|
+
def test_get_callable_not_found():
|
119
|
+
from hydraflow.executor.job import get_callable
|
119
120
|
|
120
|
-
job.call = "hydraflow.invalid"
|
121
121
|
with pytest.raises(ValueError):
|
122
|
-
|
122
|
+
get_callable("hydraflow.invalid")
|
123
123
|
|
124
124
|
|
125
125
|
def test_to_text(job: Job):
|
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
|