hydraflow 0.13.0__py3-none-any.whl → 0.14.1__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 +52 -48
- hydraflow/executor/aio.py +85 -0
- hydraflow/executor/conf.py +1 -0
- hydraflow/executor/job.py +13 -65
- {hydraflow-0.13.0.dist-info → hydraflow-0.14.1.dist-info}/METADATA +4 -4
- {hydraflow-0.13.0.dist-info → hydraflow-0.14.1.dist-info}/RECORD +9 -8
- {hydraflow-0.13.0.dist-info → hydraflow-0.14.1.dist-info}/WHEEL +0 -0
- {hydraflow-0.13.0.dist-info → hydraflow-0.14.1.dist-info}/entry_points.txt +0 -0
- {hydraflow-0.13.0.dist-info → hydraflow-0.14.1.dist-info}/licenses/LICENSE +0 -0
hydraflow/cli.py
CHANGED
@@ -3,18 +3,19 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
5
|
import shlex
|
6
|
-
from typing import Annotated
|
6
|
+
from typing import TYPE_CHECKING, Annotated
|
7
7
|
|
8
8
|
import typer
|
9
|
-
from
|
10
|
-
|
9
|
+
from typer import Argument, Exit, Option
|
10
|
+
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from hydraflow.executor.conf import Job
|
11
13
|
|
12
14
|
app = typer.Typer(add_completion=False)
|
13
|
-
console = Console()
|
14
15
|
|
15
16
|
|
16
|
-
@app.command(context_settings={"ignore_unknown_options": True})
|
17
|
-
def
|
17
|
+
@app.command("run", context_settings={"ignore_unknown_options": True})
|
18
|
+
def _run(
|
18
19
|
name: Annotated[str, Argument(help="Job name.", show_default=False)],
|
19
20
|
*,
|
20
21
|
args: Annotated[
|
@@ -28,65 +29,68 @@ def run(
|
|
28
29
|
) -> None:
|
29
30
|
"""Run a job."""
|
30
31
|
from hydraflow.executor.io import get_job
|
31
|
-
from hydraflow.executor.job import iter_batches, iter_calls, iter_runs
|
32
32
|
|
33
33
|
args = args or []
|
34
34
|
job = get_job(name)
|
35
35
|
|
36
|
-
if
|
37
|
-
|
38
|
-
|
36
|
+
if not dry_run:
|
37
|
+
import mlflow
|
38
|
+
|
39
|
+
mlflow.set_experiment(job.name)
|
40
|
+
|
41
|
+
if job.submit:
|
42
|
+
submit(job, args, dry_run=dry_run)
|
43
|
+
|
44
|
+
elif job.run:
|
45
|
+
run(job, args, dry_run=dry_run)
|
46
|
+
|
39
47
|
elif job.call:
|
40
|
-
|
41
|
-
|
48
|
+
call(job, args, dry_run=dry_run)
|
49
|
+
|
42
50
|
else:
|
43
51
|
typer.echo(f"No command found in job: {job.name}.")
|
44
|
-
raise
|
52
|
+
raise Exit(1)
|
45
53
|
|
46
|
-
if not dry_run:
|
47
|
-
import mlflow
|
48
54
|
|
49
|
-
|
55
|
+
def run(job: Job, args: list[str], *, dry_run: bool) -> None:
|
56
|
+
"""Run a job."""
|
57
|
+
from hydraflow.executor import aio
|
58
|
+
from hydraflow.executor.job import iter_batches, iter_tasks
|
50
59
|
|
51
|
-
|
52
|
-
|
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}])")
|
60
|
+
args = [*shlex.split(job.run), *args]
|
61
|
+
it = iter_tasks(args, iter_batches(job))
|
58
62
|
|
63
|
+
if not dry_run:
|
64
|
+
aio.run(it)
|
65
|
+
raise Exit
|
59
66
|
|
60
|
-
|
61
|
-
|
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
|
67
|
+
for task in it:
|
68
|
+
typer.echo(shlex.join(task.args))
|
76
69
|
|
77
|
-
args = args or []
|
78
|
-
job = get_job(name)
|
79
70
|
|
80
|
-
|
81
|
-
|
82
|
-
|
71
|
+
def call(job: Job, args: list[str], *, dry_run: bool) -> None:
|
72
|
+
"""Call a job."""
|
73
|
+
from hydraflow.executor.job import iter_batches, iter_calls
|
74
|
+
|
75
|
+
args = [*shlex.split(job.call), *args]
|
76
|
+
it = iter_calls(args, iter_batches(job))
|
83
77
|
|
84
78
|
if not dry_run:
|
85
|
-
|
79
|
+
for call in it:
|
80
|
+
call.func()
|
81
|
+
raise Exit
|
86
82
|
|
87
|
-
|
83
|
+
for task in it:
|
84
|
+
funcname, *args = task.args
|
85
|
+
arg = ", ".join(f"{arg!r}" for arg in args)
|
86
|
+
typer.echo(f"{funcname}([{arg}])")
|
88
87
|
|
89
|
-
|
88
|
+
|
89
|
+
def submit(job: Job, args: list[str], *, dry_run: bool) -> None:
|
90
|
+
"""Submit a job."""
|
91
|
+
from hydraflow.executor.job import iter_batches, submit
|
92
|
+
|
93
|
+
args = [*shlex.split(job.submit), *args]
|
90
94
|
result = submit(args, iter_batches(job), dry_run=dry_run)
|
91
95
|
|
92
96
|
if dry_run and isinstance(result, tuple):
|
@@ -124,4 +128,4 @@ def callback(
|
|
124
128
|
import importlib.metadata
|
125
129
|
|
126
130
|
typer.echo(f"hydraflow {importlib.metadata.version('hydraflow')}")
|
127
|
-
raise
|
131
|
+
raise Exit
|
@@ -0,0 +1,85 @@
|
|
1
|
+
"""Asynchronous execution."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import asyncio
|
6
|
+
from asyncio.subprocess import PIPE
|
7
|
+
from typing import TYPE_CHECKING
|
8
|
+
|
9
|
+
from rich.console import Console
|
10
|
+
from rich.progress import (
|
11
|
+
BarColumn,
|
12
|
+
MofNCompleteColumn,
|
13
|
+
Progress,
|
14
|
+
SpinnerColumn,
|
15
|
+
TaskProgressColumn,
|
16
|
+
TimeElapsedColumn,
|
17
|
+
TimeRemainingColumn,
|
18
|
+
)
|
19
|
+
|
20
|
+
if TYPE_CHECKING:
|
21
|
+
from asyncio.streams import StreamReader
|
22
|
+
from collections.abc import Callable, Iterable
|
23
|
+
|
24
|
+
from hydraflow.executor.job import Task
|
25
|
+
|
26
|
+
|
27
|
+
console = Console(log_time=False, log_path=False)
|
28
|
+
|
29
|
+
|
30
|
+
def run(iterable: Iterable[Task]) -> int | None:
|
31
|
+
"""Run multiple tasks."""
|
32
|
+
with Progress(
|
33
|
+
SpinnerColumn(),
|
34
|
+
TimeElapsedColumn(),
|
35
|
+
BarColumn(),
|
36
|
+
TimeRemainingColumn(),
|
37
|
+
MofNCompleteColumn(),
|
38
|
+
TaskProgressColumn(),
|
39
|
+
console=console,
|
40
|
+
) as progress:
|
41
|
+
|
42
|
+
def stdout(output: str) -> None:
|
43
|
+
progress.log(output.rstrip())
|
44
|
+
|
45
|
+
def stderr(output: str) -> None:
|
46
|
+
progress.log(f"[red]{output}".rstrip())
|
47
|
+
|
48
|
+
task_id = progress.add_task("")
|
49
|
+
|
50
|
+
for task in iterable:
|
51
|
+
progress.update(task_id, total=task.total)
|
52
|
+
|
53
|
+
coro = arun(task.args, stdout, stderr)
|
54
|
+
returncode = asyncio.run(coro)
|
55
|
+
|
56
|
+
if returncode:
|
57
|
+
return returncode
|
58
|
+
|
59
|
+
progress.update(task_id, completed=task.index + 1)
|
60
|
+
|
61
|
+
return 0
|
62
|
+
|
63
|
+
|
64
|
+
async def arun(
|
65
|
+
args: list[str],
|
66
|
+
stdout: Callable[[str], None],
|
67
|
+
stderr: Callable[[str], None],
|
68
|
+
) -> int | None:
|
69
|
+
"""Run a command asynchronously."""
|
70
|
+
process = await asyncio.create_subprocess_exec(*args, stdout=PIPE, stderr=PIPE)
|
71
|
+
coros = alog(process.stdout, stdout), alog(process.stderr, stderr) # type:ignore
|
72
|
+
await asyncio.gather(*coros)
|
73
|
+
await process.communicate()
|
74
|
+
|
75
|
+
return process.returncode
|
76
|
+
|
77
|
+
|
78
|
+
async def alog(reader: StreamReader, write: Callable[[str], None]) -> None:
|
79
|
+
"""Log a stream of output asynchronously."""
|
80
|
+
while True:
|
81
|
+
if reader.at_eof():
|
82
|
+
break
|
83
|
+
|
84
|
+
if out := await reader.readline():
|
85
|
+
write(out.decode())
|
hydraflow/executor/conf.py
CHANGED
hydraflow/executor/job.py
CHANGED
@@ -25,7 +25,7 @@ from dataclasses import dataclass
|
|
25
25
|
from pathlib import Path
|
26
26
|
from subprocess import CompletedProcess
|
27
27
|
from tempfile import NamedTemporaryFile
|
28
|
-
from typing import TYPE_CHECKING
|
28
|
+
from typing import TYPE_CHECKING
|
29
29
|
|
30
30
|
import ulid
|
31
31
|
|
@@ -86,47 +86,22 @@ def iter_batches(job: Job) -> Iterator[list[str]]:
|
|
86
86
|
|
87
87
|
@dataclass
|
88
88
|
class Task:
|
89
|
-
"""
|
89
|
+
"""A task to be executed."""
|
90
90
|
|
91
91
|
args: list[str]
|
92
92
|
total: int
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
@dataclass
|
97
|
-
class Run(Task):
|
98
|
-
"""An executed run."""
|
99
|
-
|
100
|
-
result: CompletedProcess
|
93
|
+
index: int
|
101
94
|
|
102
95
|
|
103
96
|
@dataclass
|
104
97
|
class Call(Task):
|
105
|
-
"""
|
98
|
+
"""A call to be executed."""
|
106
99
|
|
107
|
-
|
100
|
+
func: Callable[[], Any]
|
108
101
|
|
109
102
|
|
110
|
-
|
111
|
-
|
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."""
|
103
|
+
def iter_tasks(args: list[str], iterable: Iterable[list[str]]) -> Iterator[Task]:
|
104
|
+
"""Yield tasks of a job to be executed using a shell command."""
|
130
105
|
executable, *args = args
|
131
106
|
if executable == "python" and sys.platform == "win32":
|
132
107
|
executable = sys.executable
|
@@ -134,48 +109,21 @@ def iter_runs(
|
|
134
109
|
iterable = list(iterable)
|
135
110
|
total = len(iterable)
|
136
111
|
|
137
|
-
for
|
138
|
-
|
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]: ...
|
112
|
+
for index, args_ in enumerate(iterable):
|
113
|
+
yield Task([executable, *args, *args_], total, index)
|
148
114
|
|
149
115
|
|
150
|
-
|
151
|
-
|
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."""
|
116
|
+
def iter_calls(args: list[str], iterable: Iterable[list[str]]) -> Iterator[Call]:
|
117
|
+
"""Yield calls of a job to be executed using a Python function."""
|
166
118
|
funcname, *args = args
|
167
119
|
func = get_callable(funcname)
|
168
120
|
|
169
121
|
iterable = list(iterable)
|
170
122
|
total = len(iterable)
|
171
123
|
|
172
|
-
for
|
124
|
+
for index, args_ in enumerate(iterable):
|
173
125
|
cmd = [funcname, *args, *args_]
|
174
|
-
|
175
|
-
yield Task(cmd, total, completed)
|
176
|
-
else:
|
177
|
-
result = func([*args, *args_])
|
178
|
-
yield Call(cmd, total, completed, result)
|
126
|
+
yield Call(cmd, total, index, lambda x=cmd[1:]: func(x))
|
179
127
|
|
180
128
|
|
181
129
|
def submit(
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hydraflow
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.14.1
|
4
4
|
Summary: Hydraflow integrates Hydra and MLflow to manage and track machine learning experiments.
|
5
5
|
Project-URL: Documentation, https://daizutabi.github.io/hydraflow/
|
6
6
|
Project-URL: Source, https://github.com/daizutabi/hydraflow
|
@@ -38,10 +38,10 @@ Classifier: Programming Language :: Python :: 3.13
|
|
38
38
|
Requires-Python: >=3.10
|
39
39
|
Requires-Dist: hydra-core>=1.3
|
40
40
|
Requires-Dist: mlflow>=2.15
|
41
|
-
Requires-Dist: omegaconf
|
41
|
+
Requires-Dist: omegaconf>=2.3
|
42
42
|
Requires-Dist: python-ulid>=3.0.0
|
43
|
-
Requires-Dist: rich
|
44
|
-
Requires-Dist: typer
|
43
|
+
Requires-Dist: rich>=13.9
|
44
|
+
Requires-Dist: typer>=0.15
|
45
45
|
Description-Content-Type: text/markdown
|
46
46
|
|
47
47
|
# Hydraflow
|
@@ -1,5 +1,5 @@
|
|
1
1
|
hydraflow/__init__.py,sha256=f2KO2iF7um-nNmayNyEr7TWG4UICOXy7YAN1d3qu0OY,936
|
2
|
-
hydraflow/cli.py,sha256=
|
2
|
+
hydraflow/cli.py,sha256=qzeK6D9yJvtVHvBLf93HJPv19ODv8raLrtNLfDa7IOE,3205
|
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
|
@@ -13,12 +13,13 @@ 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/
|
16
|
+
hydraflow/executor/aio.py,sha256=xXsmBPIPdBlopv_1h0FdtOvoKUcuW7PQeKCV2d_lN9I,2122
|
17
|
+
hydraflow/executor/conf.py,sha256=icGbLDh86KgkyiGXwDoEkmZpgAP3X8Jmu_PYqJoTooY,423
|
17
18
|
hydraflow/executor/io.py,sha256=yZMcBVmAbPZZ82cAXhgiJfj9p8WvHmzOCMBg_vtEVek,1509
|
18
|
-
hydraflow/executor/job.py,sha256=
|
19
|
+
hydraflow/executor/job.py,sha256=JX6xX9ffvHB7IiAVIfzVRjjnWKaPDxBgqdZf4ZO14CY,4651
|
19
20
|
hydraflow/executor/parser.py,sha256=_Rfund3FDgrXitTt_znsTpgEtMDqZ_ICynaB_Zje14Q,14561
|
20
|
-
hydraflow-0.
|
21
|
-
hydraflow-0.
|
22
|
-
hydraflow-0.
|
23
|
-
hydraflow-0.
|
24
|
-
hydraflow-0.
|
21
|
+
hydraflow-0.14.1.dist-info/METADATA,sha256=6qrGuJ9JgAyzMdJ7p1WUTMvJBc7XKoi42ZCCXz__2BE,4566
|
22
|
+
hydraflow-0.14.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
23
|
+
hydraflow-0.14.1.dist-info/entry_points.txt,sha256=XI0khPbpCIUo9UPqkNEpgh-kqK3Jy8T7L2VCWOdkbSM,48
|
24
|
+
hydraflow-0.14.1.dist-info/licenses/LICENSE,sha256=IGdDrBPqz1O0v_UwCW-NJlbX9Hy9b3uJ11t28y2srmY,1062
|
25
|
+
hydraflow-0.14.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|