hydraflow 0.12.5__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.5 → hydraflow-0.13.0}/PKG-INFO +1 -1
- {hydraflow-0.12.5 → hydraflow-0.13.0}/pyproject.toml +2 -1
- hydraflow-0.13.0/src/hydraflow/cli.py +127 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/executor/conf.py +0 -1
- {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/executor/job.py +83 -54
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/cli/conftest.py +2 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/cli/hydraflow.yaml +7 -5
- hydraflow-0.13.0/tests/cli/submit.py +17 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/cli/test_run.py +37 -8
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/executor/echo.py +2 -0
- hydraflow-0.13.0/tests/executor/read.py +15 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/executor/test_job.py +16 -21
- hydraflow-0.12.5/src/hydraflow/cli.py +0 -94
- {hydraflow-0.12.5 → hydraflow-0.13.0}/.devcontainer/devcontainer.json +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/.devcontainer/postCreate.sh +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/.devcontainer/starship.toml +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/.gitattributes +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/.github/workflows/ci.yaml +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/.github/workflows/docs.yaml +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/.github/workflows/publish.yaml +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/.gitignore +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/LICENSE +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/README.md +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/apps/quickstart.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/docs/index.md +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/docs/usage/quickstart.md +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/mkdocs.yaml +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/__init__.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/core/__init__.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/core/config.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/core/context.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/core/io.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/core/main.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/core/mlflow.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/core/param.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/entities/__init__.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/entities/run_collection.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/entities/run_data.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/entities/run_info.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/executor/__init__.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/executor/io.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/executor/parser.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/src/hydraflow/py.typed +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/__init__.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/cli/__init__.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/cli/app.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/cli/test_setup.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/cli/test_show.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/cli/test_version.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/conftest.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/__init__.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/config/__init__.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/config/test_config.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/config/test_params.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/context/__init__.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/context/chdir.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/context/log_run.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/context/start_run.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/context/test_chdir.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/context/test_log_run.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/context/test_start_run.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/io/__init__.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/io/hydra_dir.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/io/test_hydra_dir.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/io/test_iter_dirs.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/io/test_run.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/__init__.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/default.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/force_new_run.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/match_overrides.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/rerun_finished.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/skip_finished.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/test_default.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/test_force_new_run.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/test_match_overrides.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/test_rerun_finished.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/main/test_skip_finished.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/param/__init__.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/param/params.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/param/test_param.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/param/test_params.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/core/test_mlflow.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/entities/__init__.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/entities/filter.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/entities/test_collection.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/entities/test_data.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/entities/test_filter.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/entities/test_info.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/entities/test_values.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/entities/values.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/executor/__init__.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/executor/conftest.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/executor/test_args.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/executor/test_conf.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/executor/test_io.py +0 -0
- {hydraflow-0.12.5 → hydraflow-0.13.0}/tests/executor/test_parser.py +0 -0
@@ -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
|
@@ -22,7 +22,10 @@ import shlex
|
|
22
22
|
import subprocess
|
23
23
|
import sys
|
24
24
|
from dataclasses import dataclass
|
25
|
-
from
|
25
|
+
from pathlib import Path
|
26
|
+
from subprocess import CompletedProcess
|
27
|
+
from tempfile import NamedTemporaryFile
|
28
|
+
from typing import TYPE_CHECKING, overload
|
26
29
|
|
27
30
|
import ulid
|
28
31
|
|
@@ -82,29 +85,49 @@ def iter_batches(job: Job) -> Iterator[list[str]]:
|
|
82
85
|
|
83
86
|
|
84
87
|
@dataclass
|
85
|
-
class
|
86
|
-
"""An executed
|
88
|
+
class Task:
|
89
|
+
"""An executed task."""
|
87
90
|
|
91
|
+
args: list[str]
|
88
92
|
total: int
|
89
93
|
completed: int
|
94
|
+
|
95
|
+
|
96
|
+
@dataclass
|
97
|
+
class Run(Task):
|
98
|
+
"""An executed run."""
|
99
|
+
|
90
100
|
result: CompletedProcess
|
91
101
|
|
92
102
|
|
93
103
|
@dataclass
|
94
|
-
class Call:
|
104
|
+
class Call(Task):
|
95
105
|
"""An executed call."""
|
96
106
|
|
97
|
-
total: int
|
98
|
-
completed: int
|
99
107
|
result: Any
|
100
108
|
|
101
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
|
+
|
102
123
|
def iter_runs(
|
103
|
-
executable: str,
|
104
124
|
args: list[str],
|
105
125
|
iterable: Iterable[list[str]],
|
106
|
-
|
126
|
+
*,
|
127
|
+
dry_run: bool = False,
|
128
|
+
) -> Iterator[Task | Run]:
|
107
129
|
"""Execute multiple runs of a job using shell commands."""
|
130
|
+
executable, *args = args
|
108
131
|
if executable == "python" and sys.platform == "win32":
|
109
132
|
executable = sys.executable
|
110
133
|
|
@@ -112,34 +135,75 @@ def iter_runs(
|
|
112
135
|
total = len(iterable)
|
113
136
|
|
114
137
|
for completed, args_ in enumerate(iterable, 1):
|
115
|
-
|
116
|
-
|
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]: ...
|
117
157
|
|
118
158
|
|
119
159
|
def iter_calls(
|
120
|
-
funcname: str,
|
121
160
|
args: list[str],
|
122
161
|
iterable: Iterable[list[str]],
|
123
|
-
|
162
|
+
*,
|
163
|
+
dry_run: bool = False,
|
164
|
+
) -> Iterator[Task | Call]:
|
124
165
|
"""Execute multiple calls of a job using Python functions."""
|
166
|
+
funcname, *args = args
|
125
167
|
func = get_callable(funcname)
|
126
168
|
|
127
169
|
iterable = list(iterable)
|
128
170
|
total = len(iterable)
|
129
171
|
|
130
172
|
for completed, args_ in enumerate(iterable, 1):
|
131
|
-
|
132
|
-
|
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)
|
133
179
|
|
134
180
|
|
135
181
|
def submit(
|
136
|
-
funcname: str,
|
137
182
|
args: list[str],
|
138
183
|
iterable: Iterable[list[str]],
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
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)
|
143
207
|
|
144
208
|
|
145
209
|
def get_callable(name: str) -> Callable:
|
@@ -156,38 +220,3 @@ def get_callable(name: str) -> Callable:
|
|
156
220
|
except (ImportError, AttributeError, ModuleNotFoundError) as e:
|
157
221
|
msg = f"Failed to import or find function: {name}"
|
158
222
|
raise ValueError(msg) from e
|
159
|
-
|
160
|
-
|
161
|
-
def to_text(job: Job) -> str:
|
162
|
-
"""Convert the job configuration to a string.
|
163
|
-
|
164
|
-
This function returns the job configuration for a given job.
|
165
|
-
|
166
|
-
Args:
|
167
|
-
job (Job): The job configuration to show.
|
168
|
-
|
169
|
-
Returns:
|
170
|
-
str: The job configuration.
|
171
|
-
|
172
|
-
"""
|
173
|
-
text = ""
|
174
|
-
|
175
|
-
it = iter_batches(job)
|
176
|
-
|
177
|
-
if job.run:
|
178
|
-
base_cmds = shlex.split(job.run)
|
179
|
-
for args in it:
|
180
|
-
cmds = " ".join([*base_cmds, *args])
|
181
|
-
text += f"{cmds}\n"
|
182
|
-
|
183
|
-
elif job.call:
|
184
|
-
text = f"call: {job.call}\n"
|
185
|
-
for args in it:
|
186
|
-
text += f"args: {args}\n"
|
187
|
-
|
188
|
-
elif job.submit:
|
189
|
-
text = f"submit: {job.submit}\n"
|
190
|
-
for args in it:
|
191
|
-
text += f"args: {args}\n"
|
192
|
-
|
193
|
-
return text.rstrip()
|
@@ -28,12 +28,14 @@ jobs:
|
|
28
28
|
- batch: name=c,d
|
29
29
|
args: count=4:6
|
30
30
|
submit:
|
31
|
-
|
31
|
+
run: python submit.py
|
32
32
|
steps:
|
33
|
-
- batch: name=a
|
34
|
-
args: count=1
|
35
|
-
- batch: name=
|
36
|
-
args: count=5
|
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
|
37
39
|
error:
|
38
40
|
steps:
|
39
41
|
- batch: name=a
|
@@ -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()
|
@@ -11,9 +11,9 @@ def test_run_args_dry_run():
|
|
11
11
|
result = runner.invoke(app, ["run", "args", "--dry-run"])
|
12
12
|
assert result.exit_code == 0
|
13
13
|
out = result.stdout
|
14
|
-
assert "
|
15
|
-
assert "count=
|
16
|
-
assert "
|
14
|
+
assert "app.py --multirun count=1,2,3 name=a,b" in out
|
15
|
+
assert "app.py --multirun count=4,5,6 name=c,d" in out
|
16
|
+
assert out.count("hydra.job.name=args") == 2
|
17
17
|
|
18
18
|
|
19
19
|
def test_run_batch_dry_run():
|
@@ -24,7 +24,7 @@ def test_run_batch_dry_run():
|
|
24
24
|
assert "name=b count=1,2" in out
|
25
25
|
assert "name=c,d count=100" in out
|
26
26
|
assert "name=e,f count=100" in out
|
27
|
-
assert "hydra.job.name=batch"
|
27
|
+
assert out.count("hydra.job.name=batch") == 4
|
28
28
|
|
29
29
|
|
30
30
|
def test_run_parallel_dry_run():
|
@@ -39,6 +39,27 @@ def test_run_parallel_dry_run():
|
|
39
39
|
assert "hydra.launcher.n_jobs=4" in lines[1]
|
40
40
|
|
41
41
|
|
42
|
+
def test_run_parallel_dry_run_extra_args():
|
43
|
+
args = ["run", "parallel", "--dry-run", "a", "--b", "--", "--dry-run"]
|
44
|
+
result = runner.invoke(app, args)
|
45
|
+
assert result.exit_code == 0
|
46
|
+
assert result.stdout.count("app.py a --b --dry-run --multirun") == 2
|
47
|
+
|
48
|
+
|
49
|
+
def test_run_echo_dry_run():
|
50
|
+
args = ["run", "echo", "--dry-run"]
|
51
|
+
result = runner.invoke(app, args)
|
52
|
+
assert result.exit_code == 0
|
53
|
+
assert result.stdout.count("typer.echo(['a', 'b', 'c', '--multirun',") == 4
|
54
|
+
|
55
|
+
|
56
|
+
def test_submit_dry_run():
|
57
|
+
args = ["submit", "submit", "--dry-run", "a", "--b", "--", "--dry-run"]
|
58
|
+
result = runner.invoke(app, args)
|
59
|
+
assert result.exit_code == 0
|
60
|
+
assert result.stdout.count("submit.py a --b --dry-run --multirun") == 4
|
61
|
+
|
62
|
+
|
42
63
|
@pytest.mark.xdist_group(name="group1")
|
43
64
|
def test_run_args():
|
44
65
|
result = runner.invoke(app, ["run", "args"])
|
@@ -77,13 +98,14 @@ def test_run_echo():
|
|
77
98
|
|
78
99
|
|
79
100
|
@pytest.mark.xdist_group(name="group4")
|
80
|
-
def
|
81
|
-
result = runner.invoke(app, ["
|
101
|
+
def test_submit():
|
102
|
+
result = runner.invoke(app, ["submit", "submit"])
|
82
103
|
assert result.exit_code == 0
|
83
104
|
out = result.stdout
|
84
105
|
lines = out.splitlines()
|
85
|
-
assert len(lines) ==
|
86
|
-
|
106
|
+
assert len(lines) == 1
|
107
|
+
run_ids = hydraflow.list_run_ids("submit")
|
108
|
+
assert len(run_ids) == 4
|
87
109
|
|
88
110
|
|
89
111
|
@pytest.mark.xdist_group(name="group5")
|
@@ -91,3 +113,10 @@ def test_run_error():
|
|
91
113
|
result = runner.invoke(app, ["run", "error"])
|
92
114
|
assert result.exit_code == 1
|
93
115
|
assert "No command found in job: error." in result.stdout
|
116
|
+
|
117
|
+
|
118
|
+
@pytest.mark.xdist_group(name="group5")
|
119
|
+
def test_submit_error():
|
120
|
+
result = runner.invoke(app, ["submit", "error"])
|
121
|
+
assert result.exit_code == 1
|
122
|
+
assert "No run found in job: error." in result.stdout
|
@@ -65,8 +65,8 @@ def test_iter_runs(job: Job, tmp_path: Path):
|
|
65
65
|
path = tmp_path / "output.txt"
|
66
66
|
file = Path(__file__).parent / "echo.py"
|
67
67
|
|
68
|
-
args = [file.as_posix(), path.as_posix()]
|
69
|
-
x = list(iter_runs(
|
68
|
+
args = ["python", file.as_posix(), path.as_posix()]
|
69
|
+
x = list(iter_runs(args, iter_batches(job)))
|
70
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
71
|
assert x[0].completed == 1
|
72
72
|
assert x[0].result.returncode == 0
|
@@ -79,7 +79,7 @@ def test_iter_runs(job: Job, tmp_path: Path):
|
|
79
79
|
def test_iter_calls(job: Job, capsys: pytest.CaptureFixture):
|
80
80
|
from hydraflow.executor.job import iter_batches, iter_calls
|
81
81
|
|
82
|
-
x = list(iter_calls("typer.echo"
|
82
|
+
x = list(iter_calls(["typer.echo"], iter_batches(job)))
|
83
83
|
out, _ = capsys.readouterr()
|
84
84
|
assert "'b=5', 'a=1,2'" in out
|
85
85
|
assert "'c=8', 'a=3,4'" in out
|
@@ -92,20 +92,25 @@ def test_iter_calls_args(job: Job, capsys: pytest.CaptureFixture):
|
|
92
92
|
from hydraflow.executor.job import iter_batches, iter_calls
|
93
93
|
|
94
94
|
job.call = "typer.echo a 'b c'"
|
95
|
-
list(iter_calls("typer.echo",
|
95
|
+
list(iter_calls(["typer.echo", "a", "b c"], iter_batches(job)))
|
96
96
|
out, _ = capsys.readouterr()
|
97
97
|
assert "['a', 'b c', '--multirun'," in out
|
98
98
|
|
99
99
|
|
100
|
-
def test_submit(job: Job,
|
100
|
+
def test_submit(job: Job, tmp_path: Path):
|
101
101
|
from hydraflow.executor.job import iter_batches, submit
|
102
102
|
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
103
|
+
path = tmp_path / "output.txt"
|
104
|
+
file = Path(__file__).parent / "read.py"
|
105
|
+
|
106
|
+
args = ["python", file.as_posix(), path.as_posix()]
|
107
|
+
submit(args, iter_batches(job))
|
108
|
+
lines = path.read_text().splitlines()
|
109
|
+
assert len(lines) == 4
|
110
|
+
assert lines[0].startswith("--multirun b=5 a=1,2 ")
|
111
|
+
assert lines[1].startswith("--multirun b=6 a=1,2 ")
|
112
|
+
assert lines[2].startswith("--multirun c=7 a=3,4 ")
|
113
|
+
assert lines[3].startswith("--multirun c=8 a=3,4 ")
|
109
114
|
|
110
115
|
|
111
116
|
def test_get_callable_error():
|
@@ -120,13 +125,3 @@ def test_get_callable_not_found():
|
|
120
125
|
|
121
126
|
with pytest.raises(ValueError):
|
122
127
|
get_callable("hydraflow.invalid")
|
123
|
-
|
124
|
-
|
125
|
-
def test_to_text(job: Job):
|
126
|
-
from hydraflow.executor.job import to_text
|
127
|
-
|
128
|
-
job.call = "typer.echo"
|
129
|
-
text = to_text(job)
|
130
|
-
assert "call: typer.echo\n" in text
|
131
|
-
assert "'b=5', 'a=1,2', 'hydra.job.name=test'" in text
|
132
|
-
assert "'c=8', 'a=3,4', 'hydra.job.name=test'" in text
|
@@ -1,94 +0,0 @@
|
|
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()
|
17
|
-
def run(
|
18
|
-
name: Annotated[str, Argument(help="Job name.", show_default=False)],
|
19
|
-
*,
|
20
|
-
dry_run: Annotated[
|
21
|
-
bool,
|
22
|
-
Option("--dry-run", help="Perform a dry run"),
|
23
|
-
] = False,
|
24
|
-
) -> None:
|
25
|
-
"""Run a job."""
|
26
|
-
|
27
|
-
from hydraflow.executor.io import get_job
|
28
|
-
from hydraflow.executor.job import (
|
29
|
-
iter_batches,
|
30
|
-
iter_calls,
|
31
|
-
iter_runs,
|
32
|
-
submit,
|
33
|
-
to_text,
|
34
|
-
)
|
35
|
-
|
36
|
-
job = get_job(name)
|
37
|
-
|
38
|
-
if dry_run:
|
39
|
-
typer.echo(to_text(job))
|
40
|
-
raise typer.Exit
|
41
|
-
|
42
|
-
import mlflow
|
43
|
-
|
44
|
-
mlflow.set_experiment(job.name)
|
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
|
63
|
-
|
64
|
-
|
65
|
-
@app.command()
|
66
|
-
def show(
|
67
|
-
name: Annotated[str, Argument(help="Job name.", show_default=False)] = "",
|
68
|
-
) -> None:
|
69
|
-
"""Show the hydraflow config."""
|
70
|
-
from omegaconf import OmegaConf
|
71
|
-
|
72
|
-
from hydraflow.executor.io import get_job, load_config
|
73
|
-
|
74
|
-
if name:
|
75
|
-
cfg = get_job(name)
|
76
|
-
else:
|
77
|
-
cfg = load_config()
|
78
|
-
|
79
|
-
typer.echo(OmegaConf.to_yaml(cfg))
|
80
|
-
|
81
|
-
|
82
|
-
@app.callback(invoke_without_command=True)
|
83
|
-
def callback(
|
84
|
-
*,
|
85
|
-
version: Annotated[
|
86
|
-
bool,
|
87
|
-
Option("--version", help="Show the version and exit."),
|
88
|
-
] = False,
|
89
|
-
) -> None:
|
90
|
-
if version:
|
91
|
-
import importlib.metadata
|
92
|
-
|
93
|
-
typer.echo(f"hydraflow {importlib.metadata.version('hydraflow')}")
|
94
|
-
raise typer.Exit
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|