hydraflow 0.14.0__py3-none-any.whl → 0.14.2__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 CHANGED
@@ -14,8 +14,8 @@ if TYPE_CHECKING:
14
14
  app = typer.Typer(add_completion=False)
15
15
 
16
16
 
17
- @app.command(context_settings={"ignore_unknown_options": True})
18
- def run(
17
+ @app.command("run", context_settings={"ignore_unknown_options": True})
18
+ def _run(
19
19
  name: Annotated[str, Argument(help="Job name.", show_default=False)],
20
20
  *,
21
21
  args: Annotated[
@@ -29,55 +29,73 @@ def run(
29
29
  ) -> None:
30
30
  """Run a job."""
31
31
  from hydraflow.executor.io import get_job
32
- from hydraflow.executor.job import iter_batches, iter_calls, iter_runs
33
32
 
34
33
  args = args or []
35
34
  job = get_job(name)
36
35
 
36
+ if not dry_run:
37
+ import mlflow
38
+
39
+ mlflow.set_experiment(job.name)
40
+
37
41
  if job.submit:
38
42
  submit(job, args, dry_run=dry_run)
39
- raise Exit
40
43
 
41
- if job.run:
42
- args = [*shlex.split(job.run), *args]
43
- it = iter_runs(args, iter_batches(job), dry_run=dry_run)
44
+ elif job.run:
45
+ run(job, args, dry_run=dry_run)
46
+
44
47
  elif job.call:
45
- args = [*shlex.split(job.call), *args]
46
- it = iter_calls(args, iter_batches(job), dry_run=dry_run)
48
+ call(job, args, dry_run=dry_run)
49
+
47
50
  else:
48
51
  typer.echo(f"No command found in job: {job.name}.")
49
52
  raise Exit(1)
50
53
 
54
+
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
59
+
60
+ args = [*shlex.split(job.run), *args]
61
+ it = iter_tasks(args, iter_batches(job))
62
+
51
63
  if not dry_run:
52
- import mlflow
64
+ aio.run(it)
65
+ raise Exit
53
66
 
54
- mlflow.set_experiment(job.name)
67
+ for task in it:
68
+ typer.echo(shlex.join(task.args))
55
69
 
56
- for task in it: # jobs will be executed here
57
- if job.run and dry_run:
58
- typer.echo(shlex.join(task.args))
59
- elif job.call and dry_run:
60
- funcname, *args = task.args
61
- arg = ", ".join(f"{arg!r}" for arg in args)
62
- typer.echo(f"{funcname}([{arg}])")
63
70
 
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
64
74
 
65
- def submit(job: Job, args: list[str], *, dry_run: bool) -> None:
66
- """Submit a job."""
67
- from hydraflow.executor.job import iter_batches, submit
75
+ args = [*shlex.split(job.call), *args]
76
+ it = iter_calls(args, iter_batches(job))
68
77
 
69
78
  if not dry_run:
70
- import mlflow
79
+ for call in it:
80
+ call.func()
81
+ raise Exit
71
82
 
72
- mlflow.set_experiment(job.name)
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}])")
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
73
92
 
74
93
  args = [*shlex.split(job.submit), *args]
75
94
  result = submit(args, iter_batches(job), dry_run=dry_run)
76
95
 
77
96
  if dry_run and isinstance(result, tuple):
78
- for line in result[1].splitlines():
79
- args = shlex.split(line)
80
- typer.echo(shlex.join([*result[0][:-1], *args]))
97
+ typer.echo(shlex.join(result[0]))
98
+ typer.echo(result[1])
81
99
 
82
100
 
83
101
  @app.command()
@@ -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/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, overload
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
- """An executed task."""
89
+ """A task to be executed."""
90
90
 
91
91
  args: list[str]
92
92
  total: int
93
- completed: int
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
- """An executed call."""
98
+ """A call to be executed."""
106
99
 
107
- result: Any
100
+ func: Callable[[], Any]
108
101
 
109
102
 
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."""
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 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]: ...
112
+ for index, args_ in enumerate(iterable):
113
+ yield Task([executable, *args, *args_], total, index)
148
114
 
149
115
 
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."""
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 completed, args_ in enumerate(iterable, 1):
124
+ for index, args_ in enumerate(iterable):
173
125
  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)
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.14.0
3
+ Version: 0.14.2
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
 
@@ -1,5 +1,5 @@
1
1
  hydraflow/__init__.py,sha256=f2KO2iF7um-nNmayNyEr7TWG4UICOXy7YAN1d3qu0OY,936
2
- hydraflow/cli.py,sha256=LrRHXiYln1OcH27Bf3Xf-5sesLl-eVV3YS8ZFJFpWa0,2942
2
+ hydraflow/cli.py,sha256=3rGr___wwp8KazjLGQ7JO_IgAMqLyMlcVSs_QJK7g0Y,3135
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/aio.py,sha256=xXsmBPIPdBlopv_1h0FdtOvoKUcuW7PQeKCV2d_lN9I,2122
16
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=bmjlqE-cE7lyNFFKj1nUhxiQHKf7DsFkCSTD9iTEQ5I,5606
19
+ hydraflow/executor/job.py,sha256=JX6xX9ffvHB7IiAVIfzVRjjnWKaPDxBgqdZf4ZO14CY,4651
19
20
  hydraflow/executor/parser.py,sha256=_Rfund3FDgrXitTt_znsTpgEtMDqZ_ICynaB_Zje14Q,14561
20
- hydraflow-0.14.0.dist-info/METADATA,sha256=8u8nSvKmLBhIfw4QWX9DpfC56wbb0_Dnhg1WTjBwazw,4540
21
- hydraflow-0.14.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
- hydraflow-0.14.0.dist-info/entry_points.txt,sha256=XI0khPbpCIUo9UPqkNEpgh-kqK3Jy8T7L2VCWOdkbSM,48
23
- hydraflow-0.14.0.dist-info/licenses/LICENSE,sha256=IGdDrBPqz1O0v_UwCW-NJlbX9Hy9b3uJ11t28y2srmY,1062
24
- hydraflow-0.14.0.dist-info/RECORD,,
21
+ hydraflow-0.14.2.dist-info/METADATA,sha256=vONAqVijZXLvD9rYh4AQKMbWbi-OllBkC_BF9iN5d08,4566
22
+ hydraflow-0.14.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ hydraflow-0.14.2.dist-info/entry_points.txt,sha256=XI0khPbpCIUo9UPqkNEpgh-kqK3Jy8T7L2VCWOdkbSM,48
24
+ hydraflow-0.14.2.dist-info/licenses/LICENSE,sha256=IGdDrBPqz1O0v_UwCW-NJlbX9Hy9b3uJ11t28y2srmY,1062
25
+ hydraflow-0.14.2.dist-info/RECORD,,