hydraflow 0.13.1__tar.gz → 0.14.1__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.13.1 → hydraflow-0.14.1}/PKG-INFO +2 -1
- {hydraflow-0.13.1 → hydraflow-0.14.1}/pyproject.toml +2 -1
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/cli.py +49 -43
- hydraflow-0.14.1/src/hydraflow/executor/aio.py +85 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/executor/conf.py +1 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/executor/job.py +13 -65
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/cli/hydraflow.yaml +1 -1
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/cli/test_run.py +12 -9
- hydraflow-0.14.1/tests/executor/test_aio.py +22 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/executor/test_job.py +10 -14
- {hydraflow-0.13.1 → hydraflow-0.14.1}/.devcontainer/devcontainer.json +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/.devcontainer/postCreate.sh +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/.devcontainer/starship.toml +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/.gitattributes +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/.github/workflows/ci.yaml +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/.github/workflows/docs.yaml +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/.github/workflows/publish.yaml +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/.gitignore +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/LICENSE +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/README.md +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/apps/quickstart.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/docs/index.md +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/docs/usage/quickstart.md +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/mkdocs.yaml +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/__init__.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/core/__init__.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/core/config.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/core/context.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/core/io.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/core/main.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/core/mlflow.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/core/param.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/entities/__init__.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/entities/run_collection.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/entities/run_data.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/entities/run_info.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/executor/__init__.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/executor/io.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/executor/parser.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/src/hydraflow/py.typed +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/__init__.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/cli/__init__.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/cli/app.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/cli/conftest.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/cli/submit.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/cli/test_setup.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/cli/test_show.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/cli/test_version.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/conftest.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/__init__.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/config/__init__.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/config/test_config.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/config/test_params.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/context/__init__.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/context/chdir.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/context/log_run.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/context/start_run.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/context/test_chdir.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/context/test_log_run.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/context/test_start_run.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/io/__init__.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/io/hydra_dir.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/io/test_hydra_dir.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/io/test_iter_dirs.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/io/test_run.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/__init__.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/default.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/force_new_run.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/match_overrides.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/rerun_finished.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/skip_finished.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/test_default.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/test_force_new_run.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/test_match_overrides.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/test_rerun_finished.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/main/test_skip_finished.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/param/__init__.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/param/params.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/param/test_param.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/param/test_params.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/core/test_mlflow.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/entities/__init__.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/entities/filter.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/entities/test_collection.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/entities/test_data.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/entities/test_filter.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/entities/test_info.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/entities/test_values.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/entities/values.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/executor/__init__.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/executor/conftest.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/executor/echo.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/executor/read.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/executor/test_args.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/executor/test_conf.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/tests/executor/test_io.py +0 -0
- {hydraflow-0.13.1 → hydraflow-0.14.1}/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.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
|
@@ -40,6 +40,7 @@ Requires-Dist: hydra-core>=1.3
|
|
40
40
|
Requires-Dist: mlflow>=2.15
|
41
41
|
Requires-Dist: omegaconf>=2.3
|
42
42
|
Requires-Dist: python-ulid>=3.0.0
|
43
|
+
Requires-Dist: rich>=13.9
|
43
44
|
Requires-Dist: typer>=0.15
|
44
45
|
Description-Content-Type: text/markdown
|
45
46
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "hydraflow"
|
7
|
-
version = "0.
|
7
|
+
version = "0.14.1"
|
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" }
|
@@ -24,6 +24,7 @@ dependencies = [
|
|
24
24
|
"mlflow>=2.15",
|
25
25
|
"omegaconf>=2.3",
|
26
26
|
"python-ulid>=3.0.0",
|
27
|
+
"rich>=13.9",
|
27
28
|
"typer>=0.15",
|
28
29
|
]
|
29
30
|
|
@@ -3,16 +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
9
|
from typer import Argument, Exit, Option
|
10
10
|
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from hydraflow.executor.conf import Job
|
13
|
+
|
11
14
|
app = typer.Typer(add_completion=False)
|
12
15
|
|
13
16
|
|
14
|
-
@app.command(context_settings={"ignore_unknown_options": True})
|
15
|
-
def
|
17
|
+
@app.command("run", context_settings={"ignore_unknown_options": True})
|
18
|
+
def _run(
|
16
19
|
name: Annotated[str, Argument(help="Job name.", show_default=False)],
|
17
20
|
*,
|
18
21
|
args: Annotated[
|
@@ -26,65 +29,68 @@ def run(
|
|
26
29
|
) -> None:
|
27
30
|
"""Run a job."""
|
28
31
|
from hydraflow.executor.io import get_job
|
29
|
-
from hydraflow.executor.job import iter_batches, iter_calls, iter_runs
|
30
32
|
|
31
33
|
args = args or []
|
32
34
|
job = get_job(name)
|
33
35
|
|
34
|
-
if
|
35
|
-
|
36
|
-
|
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
|
+
|
37
47
|
elif job.call:
|
38
|
-
|
39
|
-
|
48
|
+
call(job, args, dry_run=dry_run)
|
49
|
+
|
40
50
|
else:
|
41
51
|
typer.echo(f"No command found in job: {job.name}.")
|
42
52
|
raise Exit(1)
|
43
53
|
|
44
|
-
if not dry_run:
|
45
|
-
import mlflow
|
46
54
|
|
47
|
-
|
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
|
48
59
|
|
49
|
-
|
50
|
-
|
51
|
-
typer.echo(shlex.join(task.args))
|
52
|
-
elif job.call and dry_run:
|
53
|
-
funcname, *args = task.args
|
54
|
-
arg = ", ".join(f"{arg!r}" for arg in args)
|
55
|
-
typer.echo(f"{funcname}([{arg}])")
|
60
|
+
args = [*shlex.split(job.run), *args]
|
61
|
+
it = iter_tasks(args, iter_batches(job))
|
56
62
|
|
63
|
+
if not dry_run:
|
64
|
+
aio.run(it)
|
65
|
+
raise Exit
|
57
66
|
|
58
|
-
|
59
|
-
|
60
|
-
name: Annotated[str, Argument(help="Job name.", show_default=False)],
|
61
|
-
*,
|
62
|
-
args: Annotated[
|
63
|
-
list[str] | None,
|
64
|
-
Argument(help="Arguments to pass to the job.", show_default=False),
|
65
|
-
] = None,
|
66
|
-
dry_run: Annotated[
|
67
|
-
bool,
|
68
|
-
Option("--dry-run", help="Perform a dry run."),
|
69
|
-
] = False,
|
70
|
-
) -> None:
|
71
|
-
"""Submit a job."""
|
72
|
-
from hydraflow.executor.io import get_job
|
73
|
-
from hydraflow.executor.job import iter_batches, submit
|
67
|
+
for task in it:
|
68
|
+
typer.echo(shlex.join(task.args))
|
74
69
|
|
75
|
-
args = args or []
|
76
|
-
job = get_job(name)
|
77
70
|
|
78
|
-
|
79
|
-
|
80
|
-
|
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))
|
81
77
|
|
82
78
|
if not dry_run:
|
83
|
-
|
79
|
+
for call in it:
|
80
|
+
call.func()
|
81
|
+
raise Exit
|
84
82
|
|
85
|
-
|
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}])")
|
86
87
|
|
87
|
-
|
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]
|
88
94
|
result = submit(args, iter_batches(job), dry_run=dry_run)
|
89
95
|
|
90
96
|
if dry_run and isinstance(result, tuple):
|
@@ -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())
|
@@ -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(
|
@@ -54,7 +54,7 @@ def test_run_echo_dry_run():
|
|
54
54
|
|
55
55
|
|
56
56
|
def test_submit_dry_run():
|
57
|
-
args = ["
|
57
|
+
args = ["run", "submit", "--dry-run", "a", "--b", "--", "--dry-run"]
|
58
58
|
result = runner.invoke(app, args)
|
59
59
|
assert result.exit_code == 0
|
60
60
|
assert result.stdout.count("submit.py a --b --dry-run --multirun") == 4
|
@@ -83,6 +83,11 @@ def test_run_parallel():
|
|
83
83
|
run_ids = hydraflow.list_run_ids("parallel")
|
84
84
|
assert len(run_ids) == 8
|
85
85
|
|
86
|
+
result = runner.invoke(app, ["run", "parallel"]) # skip if already run
|
87
|
+
assert result.exit_code == 0
|
88
|
+
run_ids = hydraflow.list_run_ids("parallel")
|
89
|
+
assert len(run_ids) == 8
|
90
|
+
|
86
91
|
|
87
92
|
@pytest.mark.xdist_group(name="group4")
|
88
93
|
def test_run_echo():
|
@@ -99,7 +104,7 @@ def test_run_echo():
|
|
99
104
|
|
100
105
|
@pytest.mark.xdist_group(name="group4")
|
101
106
|
def test_submit():
|
102
|
-
result = runner.invoke(app, ["
|
107
|
+
result = runner.invoke(app, ["run", "submit"])
|
103
108
|
assert result.exit_code == 0
|
104
109
|
out = result.stdout
|
105
110
|
lines = out.splitlines()
|
@@ -107,16 +112,14 @@ def test_submit():
|
|
107
112
|
run_ids = hydraflow.list_run_ids("submit")
|
108
113
|
assert len(run_ids) == 4
|
109
114
|
|
115
|
+
result = runner.invoke(app, ["run", "submit"]) # skip if already run
|
116
|
+
assert result.exit_code == 0
|
117
|
+
run_ids = hydraflow.list_run_ids("submit")
|
118
|
+
assert len(run_ids) == 4
|
119
|
+
|
110
120
|
|
111
121
|
@pytest.mark.xdist_group(name="group5")
|
112
122
|
def test_run_error():
|
113
123
|
result = runner.invoke(app, ["run", "error"])
|
114
124
|
assert result.exit_code == 1
|
115
125
|
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
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import sys
|
2
|
+
|
3
|
+
import pytest
|
4
|
+
|
5
|
+
|
6
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows")
|
7
|
+
def test_run_returncode():
|
8
|
+
from hydraflow.executor.aio import run
|
9
|
+
from hydraflow.executor.job import Task
|
10
|
+
|
11
|
+
task = Task(args=["false"], index=0, total=1)
|
12
|
+
|
13
|
+
assert run([task]) == 1
|
14
|
+
|
15
|
+
|
16
|
+
def test_run_stderr():
|
17
|
+
from hydraflow.executor.aio import run
|
18
|
+
from hydraflow.executor.job import Task
|
19
|
+
|
20
|
+
task = Task(args=["python", "-c", "1/0"], index=0, total=1)
|
21
|
+
|
22
|
+
assert run([task]) == 1
|
@@ -59,40 +59,36 @@ def test_sweep_args(batches, i, x):
|
|
59
59
|
assert batches[i][-3] == x
|
60
60
|
|
61
61
|
|
62
|
-
def
|
63
|
-
|
62
|
+
def test_iter_tasks(job: Job, tmp_path: Path):
|
63
|
+
import subprocess
|
64
|
+
|
65
|
+
from hydraflow.executor.job import iter_batches, iter_tasks
|
64
66
|
|
65
67
|
path = tmp_path / "output.txt"
|
66
68
|
file = Path(__file__).parent / "echo.py"
|
67
69
|
|
68
70
|
args = ["python", file.as_posix(), path.as_posix()]
|
69
|
-
|
71
|
+
for task in iter_tasks(args, iter_batches(job)):
|
72
|
+
subprocess.run(task.args, check=True)
|
70
73
|
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
|
77
74
|
|
78
75
|
|
79
76
|
def test_iter_calls(job: Job, capsys: pytest.CaptureFixture):
|
80
77
|
from hydraflow.executor.job import iter_batches, iter_calls
|
81
78
|
|
82
|
-
|
79
|
+
for call in iter_calls(["typer.echo"], iter_batches(job)):
|
80
|
+
call.func()
|
83
81
|
out, _ = capsys.readouterr()
|
84
82
|
assert "'b=5', 'a=1,2'" in out
|
85
83
|
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
|
89
84
|
|
90
85
|
|
91
86
|
def test_iter_calls_args(job: Job, capsys: pytest.CaptureFixture):
|
92
87
|
from hydraflow.executor.job import iter_batches, iter_calls
|
93
88
|
|
94
89
|
job.call = "typer.echo a 'b c'"
|
95
|
-
|
90
|
+
for call in iter_calls(["typer.echo", "a", "b c"], iter_batches(job)):
|
91
|
+
call.func()
|
96
92
|
out, _ = capsys.readouterr()
|
97
93
|
assert "['a', 'b c', '--multirun'," in out
|
98
94
|
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|