hydraflow 0.12.4__tar.gz → 0.13.0__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.4 → hydraflow-0.13.0}/.github/workflows/ci.yaml +1 -2
- {hydraflow-0.12.4 → hydraflow-0.13.0}/.github/workflows/docs.yaml +14 -10
- {hydraflow-0.12.4 → hydraflow-0.13.0}/.github/workflows/publish.yaml +0 -1
- {hydraflow-0.12.4 → hydraflow-0.13.0}/PKG-INFO +1 -1
- {hydraflow-0.12.4 → hydraflow-0.13.0}/pyproject.toml +2 -1
- hydraflow-0.13.0/src/hydraflow/cli.py +127 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/src/hydraflow/core/io.py +10 -25
- hydraflow-0.13.0/src/hydraflow/executor/job.py +222 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/cli/conftest.py +2 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/cli/hydraflow.yaml +20 -0
- hydraflow-0.13.0/tests/cli/submit.py +17 -0
- hydraflow-0.13.0/tests/cli/test_run.py +122 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/io/test_iter_dirs.py +21 -18
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/test_mlflow.py +0 -7
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/executor/echo.py +2 -0
- hydraflow-0.13.0/tests/executor/read.py +15 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/executor/test_job.py +38 -43
- hydraflow-0.12.4/src/hydraflow/cli.py +0 -70
- hydraflow-0.12.4/src/hydraflow/executor/job.py +0 -168
- hydraflow-0.12.4/tests/cli/test_run.py +0 -63
- {hydraflow-0.12.4 → hydraflow-0.13.0}/.devcontainer/devcontainer.json +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/.devcontainer/postCreate.sh +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/.devcontainer/starship.toml +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/.gitattributes +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/.gitignore +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/LICENSE +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/README.md +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/apps/quickstart.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/docs/index.md +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/docs/usage/quickstart.md +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/mkdocs.yaml +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/src/hydraflow/__init__.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/src/hydraflow/core/__init__.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/src/hydraflow/core/config.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/src/hydraflow/core/context.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/src/hydraflow/core/main.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/src/hydraflow/core/mlflow.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/src/hydraflow/core/param.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/src/hydraflow/entities/__init__.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/src/hydraflow/entities/run_collection.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/src/hydraflow/entities/run_data.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/src/hydraflow/entities/run_info.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/src/hydraflow/executor/__init__.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/src/hydraflow/executor/conf.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/src/hydraflow/executor/io.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/src/hydraflow/executor/parser.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/src/hydraflow/py.typed +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/__init__.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/cli/__init__.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/cli/app.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/cli/test_setup.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/cli/test_show.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/cli/test_version.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/conftest.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/__init__.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/config/__init__.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/config/test_config.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/config/test_params.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/context/__init__.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/context/chdir.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/context/log_run.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/context/start_run.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/context/test_chdir.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/context/test_log_run.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/context/test_start_run.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/io/__init__.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/io/hydra_dir.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/io/test_hydra_dir.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/io/test_run.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/main/__init__.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/main/default.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/main/force_new_run.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/main/match_overrides.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/main/rerun_finished.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/main/skip_finished.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/main/test_default.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/main/test_force_new_run.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/main/test_match_overrides.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/main/test_rerun_finished.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/main/test_skip_finished.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/param/__init__.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/param/params.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/param/test_param.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/core/param/test_params.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/entities/__init__.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/entities/filter.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/entities/test_collection.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/entities/test_data.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/entities/test_filter.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/entities/test_info.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/entities/test_values.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/entities/values.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/executor/__init__.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/executor/conftest.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/executor/test_args.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/executor/test_conf.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/tests/executor/test_io.py +0 -0
- {hydraflow-0.12.4 → hydraflow-0.13.0}/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
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hydraflow
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.13.0
|
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
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "hydraflow"
|
7
|
-
version = "0.
|
7
|
+
version = "0.13.0"
|
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" }
|
@@ -92,6 +92,7 @@ ignore = [
|
|
92
92
|
"S603",
|
93
93
|
"SIM102",
|
94
94
|
"SIM108",
|
95
|
+
"SIM115",
|
95
96
|
"TRY003",
|
96
97
|
]
|
97
98
|
|
@@ -0,0 +1,127 @@
|
|
1
|
+
"""Hydraflow CLI."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import shlex
|
6
|
+
from typing import Annotated
|
7
|
+
|
8
|
+
import typer
|
9
|
+
from rich.console import Console
|
10
|
+
from typer import Argument, Option
|
11
|
+
|
12
|
+
app = typer.Typer(add_completion=False)
|
13
|
+
console = Console()
|
14
|
+
|
15
|
+
|
16
|
+
@app.command(context_settings={"ignore_unknown_options": True})
|
17
|
+
def run(
|
18
|
+
name: Annotated[str, Argument(help="Job name.", show_default=False)],
|
19
|
+
*,
|
20
|
+
args: Annotated[
|
21
|
+
list[str] | None,
|
22
|
+
Argument(help="Arguments to pass to the job.", show_default=False),
|
23
|
+
] = None,
|
24
|
+
dry_run: Annotated[
|
25
|
+
bool,
|
26
|
+
Option("--dry-run", help="Perform a dry run."),
|
27
|
+
] = False,
|
28
|
+
) -> None:
|
29
|
+
"""Run a job."""
|
30
|
+
from hydraflow.executor.io import get_job
|
31
|
+
from hydraflow.executor.job import iter_batches, iter_calls, iter_runs
|
32
|
+
|
33
|
+
args = args or []
|
34
|
+
job = get_job(name)
|
35
|
+
|
36
|
+
if job.run:
|
37
|
+
args = [*shlex.split(job.run), *args]
|
38
|
+
it = iter_runs(args, iter_batches(job), dry_run=dry_run)
|
39
|
+
elif job.call:
|
40
|
+
args = [*shlex.split(job.call), *args]
|
41
|
+
it = iter_calls(args, iter_batches(job), dry_run=dry_run)
|
42
|
+
else:
|
43
|
+
typer.echo(f"No command found in job: {job.name}.")
|
44
|
+
raise typer.Exit(1)
|
45
|
+
|
46
|
+
if not dry_run:
|
47
|
+
import mlflow
|
48
|
+
|
49
|
+
mlflow.set_experiment(job.name)
|
50
|
+
|
51
|
+
for task in it: # jobs will be executed here
|
52
|
+
if job.run and dry_run:
|
53
|
+
typer.echo(shlex.join(task.args))
|
54
|
+
elif job.call and dry_run:
|
55
|
+
funcname, *args = task.args
|
56
|
+
arg = ", ".join(f"{arg!r}" for arg in args)
|
57
|
+
typer.echo(f"{funcname}([{arg}])")
|
58
|
+
|
59
|
+
|
60
|
+
@app.command(context_settings={"ignore_unknown_options": True})
|
61
|
+
def submit(
|
62
|
+
name: Annotated[str, Argument(help="Job name.", show_default=False)],
|
63
|
+
*,
|
64
|
+
args: Annotated[
|
65
|
+
list[str] | None,
|
66
|
+
Argument(help="Arguments to pass to the job.", show_default=False),
|
67
|
+
] = None,
|
68
|
+
dry_run: Annotated[
|
69
|
+
bool,
|
70
|
+
Option("--dry-run", help="Perform a dry run."),
|
71
|
+
] = False,
|
72
|
+
) -> None:
|
73
|
+
"""Submit a job."""
|
74
|
+
from hydraflow.executor.io import get_job
|
75
|
+
from hydraflow.executor.job import iter_batches, submit
|
76
|
+
|
77
|
+
args = args or []
|
78
|
+
job = get_job(name)
|
79
|
+
|
80
|
+
if not job.run:
|
81
|
+
typer.echo(f"No run found in job: {job.name}.")
|
82
|
+
raise typer.Exit(1)
|
83
|
+
|
84
|
+
if not dry_run:
|
85
|
+
import mlflow
|
86
|
+
|
87
|
+
mlflow.set_experiment(job.name)
|
88
|
+
|
89
|
+
args = [*shlex.split(job.run), *args]
|
90
|
+
result = submit(args, iter_batches(job), dry_run=dry_run)
|
91
|
+
|
92
|
+
if dry_run and isinstance(result, tuple):
|
93
|
+
for line in result[1].splitlines():
|
94
|
+
args = shlex.split(line)
|
95
|
+
typer.echo(shlex.join([*result[0][:-1], *args]))
|
96
|
+
|
97
|
+
|
98
|
+
@app.command()
|
99
|
+
def show(
|
100
|
+
name: Annotated[str, Argument(help="Job name.", show_default=False)] = "",
|
101
|
+
) -> None:
|
102
|
+
"""Show the hydraflow config."""
|
103
|
+
from omegaconf import OmegaConf
|
104
|
+
|
105
|
+
from hydraflow.executor.io import get_job, load_config
|
106
|
+
|
107
|
+
if name:
|
108
|
+
cfg = get_job(name)
|
109
|
+
else:
|
110
|
+
cfg = load_config()
|
111
|
+
|
112
|
+
typer.echo(OmegaConf.to_yaml(cfg))
|
113
|
+
|
114
|
+
|
115
|
+
@app.callback(invoke_without_command=True)
|
116
|
+
def callback(
|
117
|
+
*,
|
118
|
+
version: Annotated[
|
119
|
+
bool,
|
120
|
+
Option("--version", help="Show the version and exit."),
|
121
|
+
] = False,
|
122
|
+
) -> None:
|
123
|
+
if version:
|
124
|
+
import importlib.metadata
|
125
|
+
|
126
|
+
typer.echo(f"hydraflow {importlib.metadata.version('hydraflow')}")
|
127
|
+
raise typer.Exit
|
@@ -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
|
@@ -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"
|
@@ -195,50 +181,49 @@ def predicate_experiment_dir(
|
|
195
181
|
return True
|
196
182
|
|
197
183
|
if isinstance(experiment_names, list):
|
198
|
-
return name in experiment_names
|
184
|
+
return any(fnmatch.fnmatch(name, e) for e in experiment_names)
|
199
185
|
|
200
186
|
return experiment_names(name)
|
201
187
|
|
202
188
|
|
203
189
|
def iter_experiment_dirs(
|
190
|
+
root_dir: str | Path,
|
204
191
|
experiment_names: str | list[str] | Callable[[str], bool] | None = None,
|
205
|
-
root_dir: str | Path | None = None,
|
206
192
|
) -> Iterator[Path]:
|
207
193
|
"""Iterate over the experiment directories in the root directory."""
|
208
194
|
if isinstance(experiment_names, str):
|
209
195
|
experiment_names = [experiment_names]
|
210
196
|
|
211
|
-
|
212
|
-
for path in root_dir.iterdir():
|
197
|
+
for path in Path(root_dir).iterdir():
|
213
198
|
if predicate_experiment_dir(path, experiment_names):
|
214
199
|
yield path
|
215
200
|
|
216
201
|
|
217
202
|
def iter_run_dirs(
|
203
|
+
root_dir: str | Path,
|
218
204
|
experiment_names: str | list[str] | Callable[[str], bool] | None = None,
|
219
|
-
root_dir: str | Path | None = None,
|
220
205
|
) -> Iterator[Path]:
|
221
206
|
"""Iterate over the run directories in the root directory."""
|
222
|
-
for experiment_dir in iter_experiment_dirs(
|
207
|
+
for experiment_dir in iter_experiment_dirs(root_dir, experiment_names):
|
223
208
|
for path in experiment_dir.iterdir():
|
224
209
|
if path.is_dir() and (path / "artifacts").exists():
|
225
210
|
yield path
|
226
211
|
|
227
212
|
|
228
213
|
def iter_artifacts_dirs(
|
214
|
+
root_dir: str | Path,
|
229
215
|
experiment_names: str | list[str] | Callable[[str], bool] | None = None,
|
230
|
-
root_dir: str | Path | None = None,
|
231
216
|
) -> Iterator[Path]:
|
232
217
|
"""Iterate over the artifacts directories in the root directory."""
|
233
|
-
for path in iter_run_dirs(
|
218
|
+
for path in iter_run_dirs(root_dir, experiment_names):
|
234
219
|
yield path / "artifacts"
|
235
220
|
|
236
221
|
|
237
222
|
def iter_artifact_paths(
|
223
|
+
root_dir: str | Path,
|
238
224
|
artifact_path: str | Path,
|
239
225
|
experiment_names: str | list[str] | Callable[[str], bool] | None = None,
|
240
|
-
root_dir: str | Path | None = None,
|
241
226
|
) -> Iterator[Path]:
|
242
227
|
"""Iterate over the artifact paths in the root directory."""
|
243
|
-
for path in iter_artifacts_dirs(
|
228
|
+
for path in iter_artifacts_dirs(root_dir, experiment_names):
|
244
229
|
yield path / artifact_path
|
@@ -0,0 +1,222 @@
|
|
1
|
+
"""Job execution and argument handling for HydraFlow.
|
2
|
+
|
3
|
+
This module provides functionality for executing jobs in HydraFlow, including:
|
4
|
+
|
5
|
+
- Argument parsing and expansion for job steps
|
6
|
+
- Batch processing of Hydra configurations
|
7
|
+
- Execution of jobs via shell commands or Python functions
|
8
|
+
|
9
|
+
The module supports two execution modes:
|
10
|
+
|
11
|
+
1. Shell command execution
|
12
|
+
2. Python function calls
|
13
|
+
|
14
|
+
Each job can consist of multiple steps, and each step can have its own
|
15
|
+
arguments and configurations that will be expanded into multiple runs.
|
16
|
+
"""
|
17
|
+
|
18
|
+
from __future__ import annotations
|
19
|
+
|
20
|
+
import importlib
|
21
|
+
import shlex
|
22
|
+
import subprocess
|
23
|
+
import sys
|
24
|
+
from dataclasses import dataclass
|
25
|
+
from pathlib import Path
|
26
|
+
from subprocess import CompletedProcess
|
27
|
+
from tempfile import NamedTemporaryFile
|
28
|
+
from typing import TYPE_CHECKING, overload
|
29
|
+
|
30
|
+
import ulid
|
31
|
+
|
32
|
+
from .parser import collect, expand
|
33
|
+
|
34
|
+
if TYPE_CHECKING:
|
35
|
+
from collections.abc import Callable, Iterable, Iterator
|
36
|
+
from subprocess import CompletedProcess
|
37
|
+
from typing import Any
|
38
|
+
|
39
|
+
from .conf import Job
|
40
|
+
|
41
|
+
|
42
|
+
def iter_args(batch: str, args: str) -> Iterator[list[str]]:
|
43
|
+
"""Iterate over combinations generated from parsed arguments.
|
44
|
+
|
45
|
+
Generate all possible combinations of arguments by parsing and
|
46
|
+
expanding each one, yielding them as an iterator.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
batch (str): The batch to parse.
|
50
|
+
args (str): The arguments to parse.
|
51
|
+
|
52
|
+
Yields:
|
53
|
+
list[str]: a list of the parsed argument combinations.
|
54
|
+
|
55
|
+
"""
|
56
|
+
args_ = collect(args)
|
57
|
+
|
58
|
+
for batch_ in expand(batch):
|
59
|
+
yield [*batch_, *args_]
|
60
|
+
|
61
|
+
|
62
|
+
def iter_batches(job: Job) -> Iterator[list[str]]:
|
63
|
+
"""Generate Hydra application arguments for a job.
|
64
|
+
|
65
|
+
This function generates a list of Hydra application arguments
|
66
|
+
for a given job, including the job name and the root directory
|
67
|
+
for the sweep.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
job (Job): The job to generate the Hydra configuration for.
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
list[str]: A list of Hydra configuration strings.
|
74
|
+
|
75
|
+
"""
|
76
|
+
job_name = f"hydra.job.name={job.name}"
|
77
|
+
job_configs = shlex.split(job.with_)
|
78
|
+
|
79
|
+
for step in job.steps:
|
80
|
+
configs = shlex.split(step.with_) or job_configs
|
81
|
+
|
82
|
+
for args in iter_args(step.batch, step.args):
|
83
|
+
sweep_dir = f"hydra.sweep.dir=multirun/{ulid.ULID()}"
|
84
|
+
yield ["--multirun", *args, job_name, sweep_dir, *configs]
|
85
|
+
|
86
|
+
|
87
|
+
@dataclass
|
88
|
+
class Task:
|
89
|
+
"""An executed task."""
|
90
|
+
|
91
|
+
args: list[str]
|
92
|
+
total: int
|
93
|
+
completed: int
|
94
|
+
|
95
|
+
|
96
|
+
@dataclass
|
97
|
+
class Run(Task):
|
98
|
+
"""An executed run."""
|
99
|
+
|
100
|
+
result: CompletedProcess
|
101
|
+
|
102
|
+
|
103
|
+
@dataclass
|
104
|
+
class Call(Task):
|
105
|
+
"""An executed call."""
|
106
|
+
|
107
|
+
result: Any
|
108
|
+
|
109
|
+
|
110
|
+
@overload
|
111
|
+
def iter_runs(args: list[str], iterable: Iterable[list[str]]) -> Iterator[Run]: ...
|
112
|
+
|
113
|
+
|
114
|
+
@overload
|
115
|
+
def iter_runs(
|
116
|
+
args: list[str],
|
117
|
+
iterable: Iterable[list[str]],
|
118
|
+
*,
|
119
|
+
dry_run: bool = False,
|
120
|
+
) -> Iterator[Task | Run]: ...
|
121
|
+
|
122
|
+
|
123
|
+
def iter_runs(
|
124
|
+
args: list[str],
|
125
|
+
iterable: Iterable[list[str]],
|
126
|
+
*,
|
127
|
+
dry_run: bool = False,
|
128
|
+
) -> Iterator[Task | Run]:
|
129
|
+
"""Execute multiple runs of a job using shell commands."""
|
130
|
+
executable, *args = args
|
131
|
+
if executable == "python" and sys.platform == "win32":
|
132
|
+
executable = sys.executable
|
133
|
+
|
134
|
+
iterable = list(iterable)
|
135
|
+
total = len(iterable)
|
136
|
+
|
137
|
+
for completed, args_ in enumerate(iterable, 1):
|
138
|
+
cmd = [executable, *args, *args_]
|
139
|
+
if dry_run:
|
140
|
+
yield Task(cmd, total, completed)
|
141
|
+
else:
|
142
|
+
result = subprocess.run(cmd, check=False)
|
143
|
+
yield Run(cmd, total, completed, result)
|
144
|
+
|
145
|
+
|
146
|
+
@overload
|
147
|
+
def iter_calls(args: list[str], iterable: Iterable[list[str]]) -> Iterator[Call]: ...
|
148
|
+
|
149
|
+
|
150
|
+
@overload
|
151
|
+
def iter_calls(
|
152
|
+
args: list[str],
|
153
|
+
iterable: Iterable[list[str]],
|
154
|
+
*,
|
155
|
+
dry_run: bool = False,
|
156
|
+
) -> Iterator[Task | Call]: ...
|
157
|
+
|
158
|
+
|
159
|
+
def iter_calls(
|
160
|
+
args: list[str],
|
161
|
+
iterable: Iterable[list[str]],
|
162
|
+
*,
|
163
|
+
dry_run: bool = False,
|
164
|
+
) -> Iterator[Task | Call]:
|
165
|
+
"""Execute multiple calls of a job using Python functions."""
|
166
|
+
funcname, *args = args
|
167
|
+
func = get_callable(funcname)
|
168
|
+
|
169
|
+
iterable = list(iterable)
|
170
|
+
total = len(iterable)
|
171
|
+
|
172
|
+
for completed, args_ in enumerate(iterable, 1):
|
173
|
+
cmd = [funcname, *args, *args_]
|
174
|
+
if dry_run:
|
175
|
+
yield Task(cmd, total, completed)
|
176
|
+
else:
|
177
|
+
result = func([*args, *args_])
|
178
|
+
yield Call(cmd, total, completed, result)
|
179
|
+
|
180
|
+
|
181
|
+
def submit(
|
182
|
+
args: list[str],
|
183
|
+
iterable: Iterable[list[str]],
|
184
|
+
*,
|
185
|
+
dry_run: bool = False,
|
186
|
+
) -> CompletedProcess | tuple[list[str], str]:
|
187
|
+
"""Submit entire job using a shell command."""
|
188
|
+
executable, *args = args
|
189
|
+
if executable == "python" and sys.platform == "win32":
|
190
|
+
executable = sys.executable
|
191
|
+
|
192
|
+
temp = NamedTemporaryFile(dir=Path.cwd(), delete=False) # for Windows
|
193
|
+
file = Path(temp.name)
|
194
|
+
temp.close()
|
195
|
+
|
196
|
+
text = "\n".join(shlex.join(args) for args in iterable)
|
197
|
+
file.write_text(text)
|
198
|
+
cmd = [executable, *args, file.as_posix()]
|
199
|
+
|
200
|
+
try:
|
201
|
+
if dry_run:
|
202
|
+
return cmd, text
|
203
|
+
return subprocess.run(cmd, check=False)
|
204
|
+
|
205
|
+
finally:
|
206
|
+
file.unlink(missing_ok=True)
|
207
|
+
|
208
|
+
|
209
|
+
def get_callable(name: str) -> Callable:
|
210
|
+
"""Get a callable from a function name."""
|
211
|
+
if "." not in name:
|
212
|
+
msg = f"Invalid function path: {name}."
|
213
|
+
raise ValueError(msg)
|
214
|
+
|
215
|
+
try:
|
216
|
+
module_name, func_name = name.rsplit(".", 1)
|
217
|
+
module = importlib.import_module(module_name)
|
218
|
+
return getattr(module, func_name)
|
219
|
+
|
220
|
+
except (ImportError, AttributeError, ModuleNotFoundError) as e:
|
221
|
+
msg = f"Failed to import or find function: {name}"
|
222
|
+
raise ValueError(msg) from e
|
@@ -20,3 +20,23 @@ 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
|
+
run: python submit.py
|
32
|
+
steps:
|
33
|
+
- batch: name=a,b
|
34
|
+
args: count=1
|
35
|
+
- batch: name=c
|
36
|
+
args: count=5
|
37
|
+
- batch: name=d
|
38
|
+
args: count=6
|
39
|
+
error:
|
40
|
+
steps:
|
41
|
+
- batch: name=a
|
42
|
+
args: count=1:3
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import shlex
|
4
|
+
import subprocess
|
5
|
+
import sys
|
6
|
+
from pathlib import Path
|
7
|
+
|
8
|
+
|
9
|
+
def main():
|
10
|
+
file = Path(sys.argv[-1])
|
11
|
+
for line in file.read_text().splitlines():
|
12
|
+
args = shlex.split(line)
|
13
|
+
subprocess.run([sys.executable, "app.py", *args], check=False)
|
14
|
+
|
15
|
+
|
16
|
+
if __name__ == "__main__":
|
17
|
+
main()
|