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 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 rich.console import Console
10
- from typer import Argument, Option
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 run(
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 job.run:
37
- args = [*shlex.split(job.run), *args]
38
- it = iter_runs(args, iter_batches(job), dry_run=dry_run)
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
- args = [*shlex.split(job.call), *args]
41
- it = iter_calls(args, iter_batches(job), dry_run=dry_run)
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 typer.Exit(1)
52
+ raise Exit(1)
45
53
 
46
- if not dry_run:
47
- import mlflow
48
54
 
49
- mlflow.set_experiment(job.name)
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
- 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}])")
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
- @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
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
- if not job.run:
81
- typer.echo(f"No run found in job: {job.name}.")
82
- raise typer.Exit(1)
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
- import mlflow
79
+ for call in it:
80
+ call.func()
81
+ raise Exit
86
82
 
87
- 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}])")
88
87
 
89
- args = [*shlex.split(job.run), *args]
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 typer.Exit
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())
@@ -15,6 +15,7 @@ class Job:
15
15
  name: str = ""
16
16
  run: str = ""
17
17
  call: str = ""
18
+ submit: str = ""
18
19
  with_: str = ""
19
20
  steps: list[Step] = field(default_factory=list)
20
21
 
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.13.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=nGFrZeQnn1h7lCbVhjBJwdrcDDGoqYpVFbqqwkLMzng,3385
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/conf.py,sha256=2dv6_PlsynRmia-fGZlmBEVt8GopT0f32N13qY7tYnM,402
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=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.13.0.dist-info/METADATA,sha256=DcBL4IuQHdGIyEXTqFoDGlUdI2sgH0THpTuyFoB3Wg0,4549
21
- hydraflow-0.13.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
- hydraflow-0.13.0.dist-info/entry_points.txt,sha256=XI0khPbpCIUo9UPqkNEpgh-kqK3Jy8T7L2VCWOdkbSM,48
23
- hydraflow-0.13.0.dist-info/licenses/LICENSE,sha256=IGdDrBPqz1O0v_UwCW-NJlbX9Hy9b3uJ11t28y2srmY,1062
24
- hydraflow-0.13.0.dist-info/RECORD,,
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,,