hydraflow 0.12.3__py3-none-any.whl → 0.12.5__py3-none-any.whl
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/cli.py +26 -2
- hydraflow/core/io.py +37 -33
- hydraflow/executor/conf.py +1 -0
- hydraflow/executor/job.py +73 -48
- {hydraflow-0.12.3.dist-info → hydraflow-0.12.5.dist-info}/METADATA +1 -1
- {hydraflow-0.12.3.dist-info → hydraflow-0.12.5.dist-info}/RECORD +9 -9
- {hydraflow-0.12.3.dist-info → hydraflow-0.12.5.dist-info}/WHEEL +0 -0
- {hydraflow-0.12.3.dist-info → hydraflow-0.12.5.dist-info}/entry_points.txt +0 -0
- {hydraflow-0.12.3.dist-info → hydraflow-0.12.5.dist-info}/licenses/LICENSE +0 -0
hydraflow/cli.py
CHANGED
@@ -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()
|
hydraflow/core/io.py
CHANGED
@@ -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
|
hydraflow/executor/conf.py
CHANGED
hydraflow/executor/job.py
CHANGED
@@ -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()
|
@@ -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
1
|
hydraflow/__init__.py,sha256=f2KO2iF7um-nNmayNyEr7TWG4UICOXy7YAN1d3qu0OY,936
|
2
|
-
hydraflow/cli.py,sha256=
|
2
|
+
hydraflow/cli.py,sha256=Z9gpuxW6sgGg8lO5nmDE7dToLnNuWVT4nk9j0opqqHs,2075
|
3
3
|
hydraflow/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
4
|
hydraflow/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
5
|
hydraflow/core/config.py,sha256=SJzjgsO_kzB78_whJ3lmy7GlZvTvwZONH1BJBn8zCuI,3817
|
6
6
|
hydraflow/core/context.py,sha256=L4OygMLbITwlWzq17Lh8VoXKKtjOJ3DBEVsBddKPSJ8,4741
|
7
|
-
hydraflow/core/io.py,sha256=
|
7
|
+
hydraflow/core/io.py,sha256=Tch85xbdRao7rG9BMbRpc2Cq0glC8a8M87QDoyQ81p8,6926
|
8
8
|
hydraflow/core/main.py,sha256=dY8uUykS_AbzverrSWkXLyj98TjBPHAiMUf_l5met1U,5162
|
9
9
|
hydraflow/core/mlflow.py,sha256=OQJ3f2wkHJRb11ZK__HF4R8FyBEje7-NOqObpoanGhU,5704
|
10
10
|
hydraflow/core/param.py,sha256=LHU9j9_7oA99igasoOyKofKClVr9FmGA3UABJ-KmyS0,4538
|
@@ -13,12 +13,12 @@ hydraflow/entities/run_collection.py,sha256=4sfZWXaS7kqnVCo9GyB3pN6BaSUCkA-ZqSSl
|
|
13
13
|
hydraflow/entities/run_data.py,sha256=lz8HPxG0iz1Jf9FU6HTFW4gcAc3N2pgMyAPqAIK6I74,1644
|
14
14
|
hydraflow/entities/run_info.py,sha256=FRC6ICOlzB2u_xi_33Qs-YZLt677UotuNbYqI7XSmHY,1017
|
15
15
|
hydraflow/executor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
|
-
hydraflow/executor/conf.py,sha256=
|
16
|
+
hydraflow/executor/conf.py,sha256=icGbLDh86KgkyiGXwDoEkmZpgAP3X8Jmu_PYqJoTooY,423
|
17
17
|
hydraflow/executor/io.py,sha256=yZMcBVmAbPZZ82cAXhgiJfj9p8WvHmzOCMBg_vtEVek,1509
|
18
|
-
hydraflow/executor/job.py,sha256=
|
18
|
+
hydraflow/executor/job.py,sha256=Ug_4iVL-rNzMvVjBDecn3FVFGLmRzJEf4Pu6LP_x3LA,4844
|
19
19
|
hydraflow/executor/parser.py,sha256=_Rfund3FDgrXitTt_znsTpgEtMDqZ_ICynaB_Zje14Q,14561
|
20
|
-
hydraflow-0.12.
|
21
|
-
hydraflow-0.12.
|
22
|
-
hydraflow-0.12.
|
23
|
-
hydraflow-0.12.
|
24
|
-
hydraflow-0.12.
|
20
|
+
hydraflow-0.12.5.dist-info/METADATA,sha256=Ztz0IFLx22RTWpPkVguyfUrsaVaZ3WIzyaA1Lwc3sYQ,4549
|
21
|
+
hydraflow-0.12.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
22
|
+
hydraflow-0.12.5.dist-info/entry_points.txt,sha256=XI0khPbpCIUo9UPqkNEpgh-kqK3Jy8T7L2VCWOdkbSM,48
|
23
|
+
hydraflow-0.12.5.dist-info/licenses/LICENSE,sha256=IGdDrBPqz1O0v_UwCW-NJlbX9Hy9b3uJ11t28y2srmY,1062
|
24
|
+
hydraflow-0.12.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|