hydraflow 0.12.5__py3-none-any.whl → 0.13.0__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
@@ -13,53 +13,86 @@ app = typer.Typer(add_completion=False)
13
13
  console = Console()
14
14
 
15
15
 
16
- @app.command()
16
+ @app.command(context_settings={"ignore_unknown_options": True})
17
17
  def run(
18
18
  name: Annotated[str, Argument(help="Job name.", show_default=False)],
19
19
  *,
20
+ args: Annotated[
21
+ list[str] | None,
22
+ Argument(help="Arguments to pass to the job.", show_default=False),
23
+ ] = None,
20
24
  dry_run: Annotated[
21
25
  bool,
22
- Option("--dry-run", help="Perform a dry run"),
26
+ Option("--dry-run", help="Perform a dry run."),
23
27
  ] = False,
24
28
  ) -> None:
25
29
  """Run a job."""
26
-
27
30
  from hydraflow.executor.io import get_job
28
- from hydraflow.executor.job import (
29
- iter_batches,
30
- iter_calls,
31
- iter_runs,
32
- submit,
33
- to_text,
34
- )
31
+ from hydraflow.executor.job import iter_batches, iter_calls, iter_runs
35
32
 
33
+ args = args or []
36
34
  job = get_job(name)
37
35
 
38
- if dry_run:
39
- typer.echo(to_text(job))
40
- raise typer.Exit
41
-
42
- import mlflow
43
-
44
- mlflow.set_experiment(job.name)
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
36
  if job.run:
52
- executable, *args = shlex.split(job.run)
53
- it = iter_runs(executable, args, iter_batches(job))
37
+ args = [*shlex.split(job.run), *args]
38
+ it = iter_runs(args, iter_batches(job), dry_run=dry_run)
54
39
  elif job.call:
55
- funcname, *args = shlex.split(job.call)
56
- it = iter_calls(funcname, args, iter_batches(job))
40
+ args = [*shlex.split(job.call), *args]
41
+ it = iter_calls(args, iter_batches(job), dry_run=dry_run)
57
42
  else:
58
43
  typer.echo(f"No command found in job: {job.name}.")
59
44
  raise typer.Exit(1)
60
45
 
61
- for _ in it:
62
- pass
46
+ if not dry_run:
47
+ import mlflow
48
+
49
+ mlflow.set_experiment(job.name)
50
+
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}])")
58
+
59
+
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
76
+
77
+ args = args or []
78
+ job = get_job(name)
79
+
80
+ if not job.run:
81
+ typer.echo(f"No run found in job: {job.name}.")
82
+ raise typer.Exit(1)
83
+
84
+ if not dry_run:
85
+ import mlflow
86
+
87
+ mlflow.set_experiment(job.name)
88
+
89
+ args = [*shlex.split(job.run), *args]
90
+ result = submit(args, iter_batches(job), dry_run=dry_run)
91
+
92
+ if dry_run and isinstance(result, tuple):
93
+ for line in result[1].splitlines():
94
+ args = shlex.split(line)
95
+ typer.echo(shlex.join([*result[0][:-1], *args]))
63
96
 
64
97
 
65
98
  @app.command()
@@ -15,7 +15,6 @@ class Job:
15
15
  name: str = ""
16
16
  run: str = ""
17
17
  call: str = ""
18
- submit: str = ""
19
18
  with_: str = ""
20
19
  steps: list[Step] = field(default_factory=list)
21
20
 
hydraflow/executor/job.py CHANGED
@@ -22,7 +22,10 @@ import shlex
22
22
  import subprocess
23
23
  import sys
24
24
  from dataclasses import dataclass
25
- from typing import TYPE_CHECKING
25
+ from pathlib import Path
26
+ from subprocess import CompletedProcess
27
+ from tempfile import NamedTemporaryFile
28
+ from typing import TYPE_CHECKING, overload
26
29
 
27
30
  import ulid
28
31
 
@@ -82,29 +85,49 @@ def iter_batches(job: Job) -> Iterator[list[str]]:
82
85
 
83
86
 
84
87
  @dataclass
85
- class Run:
86
- """An executed run."""
88
+ class Task:
89
+ """An executed task."""
87
90
 
91
+ args: list[str]
88
92
  total: int
89
93
  completed: int
94
+
95
+
96
+ @dataclass
97
+ class Run(Task):
98
+ """An executed run."""
99
+
90
100
  result: CompletedProcess
91
101
 
92
102
 
93
103
  @dataclass
94
- class Call:
104
+ class Call(Task):
95
105
  """An executed call."""
96
106
 
97
- total: int
98
- completed: int
99
107
  result: Any
100
108
 
101
109
 
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
+
102
123
  def iter_runs(
103
- executable: str,
104
124
  args: list[str],
105
125
  iterable: Iterable[list[str]],
106
- ) -> Iterator[Run]:
126
+ *,
127
+ dry_run: bool = False,
128
+ ) -> Iterator[Task | Run]:
107
129
  """Execute multiple runs of a job using shell commands."""
130
+ executable, *args = args
108
131
  if executable == "python" and sys.platform == "win32":
109
132
  executable = sys.executable
110
133
 
@@ -112,34 +135,75 @@ def iter_runs(
112
135
  total = len(iterable)
113
136
 
114
137
  for completed, args_ in enumerate(iterable, 1):
115
- result = subprocess.run([executable, *args, *args_], check=False)
116
- yield Run(total, completed, result)
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]: ...
148
+
149
+
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]: ...
117
157
 
118
158
 
119
159
  def iter_calls(
120
- funcname: str,
121
160
  args: list[str],
122
161
  iterable: Iterable[list[str]],
123
- ) -> Iterator[Call]:
162
+ *,
163
+ dry_run: bool = False,
164
+ ) -> Iterator[Task | Call]:
124
165
  """Execute multiple calls of a job using Python functions."""
166
+ funcname, *args = args
125
167
  func = get_callable(funcname)
126
168
 
127
169
  iterable = list(iterable)
128
170
  total = len(iterable)
129
171
 
130
172
  for completed, args_ in enumerate(iterable, 1):
131
- result = func([*args, *args_])
132
- yield Call(total, completed, result)
173
+ 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)
133
179
 
134
180
 
135
181
  def submit(
136
- funcname: str,
137
182
  args: list[str],
138
183
  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])
184
+ *,
185
+ dry_run: bool = False,
186
+ ) -> CompletedProcess | tuple[list[str], str]:
187
+ """Submit entire job using a shell command."""
188
+ executable, *args = args
189
+ if executable == "python" and sys.platform == "win32":
190
+ executable = sys.executable
191
+
192
+ temp = NamedTemporaryFile(dir=Path.cwd(), delete=False) # for Windows
193
+ file = Path(temp.name)
194
+ temp.close()
195
+
196
+ text = "\n".join(shlex.join(args) for args in iterable)
197
+ file.write_text(text)
198
+ cmd = [executable, *args, file.as_posix()]
199
+
200
+ try:
201
+ if dry_run:
202
+ return cmd, text
203
+ return subprocess.run(cmd, check=False)
204
+
205
+ finally:
206
+ file.unlink(missing_ok=True)
143
207
 
144
208
 
145
209
  def get_callable(name: str) -> Callable:
@@ -156,38 +220,3 @@ def get_callable(name: str) -> Callable:
156
220
  except (ImportError, AttributeError, ModuleNotFoundError) as e:
157
221
  msg = f"Failed to import or find function: {name}"
158
222
  raise ValueError(msg) from e
159
-
160
-
161
- def to_text(job: Job) -> str:
162
- """Convert the job configuration to a string.
163
-
164
- This function returns the job configuration for a given job.
165
-
166
- Args:
167
- job (Job): The job configuration to show.
168
-
169
- Returns:
170
- str: The job configuration.
171
-
172
- """
173
- text = ""
174
-
175
- it = iter_batches(job)
176
-
177
- if job.run:
178
- base_cmds = shlex.split(job.run)
179
- for args in it:
180
- cmds = " ".join([*base_cmds, *args])
181
- text += f"{cmds}\n"
182
-
183
- elif job.call:
184
- text = f"call: {job.call}\n"
185
- for args in it:
186
- text += f"args: {args}\n"
187
-
188
- elif job.submit:
189
- text = f"submit: {job.submit}\n"
190
- for args in it:
191
- text += f"args: {args}\n"
192
-
193
- return text.rstrip()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hydraflow
3
- Version: 0.12.5
3
+ Version: 0.13.0
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,5 +1,5 @@
1
1
  hydraflow/__init__.py,sha256=f2KO2iF7um-nNmayNyEr7TWG4UICOXy7YAN1d3qu0OY,936
2
- hydraflow/cli.py,sha256=Z9gpuxW6sgGg8lO5nmDE7dToLnNuWVT4nk9j0opqqHs,2075
2
+ hydraflow/cli.py,sha256=nGFrZeQnn1h7lCbVhjBJwdrcDDGoqYpVFbqqwkLMzng,3385
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,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=icGbLDh86KgkyiGXwDoEkmZpgAP3X8Jmu_PYqJoTooY,423
16
+ hydraflow/executor/conf.py,sha256=2dv6_PlsynRmia-fGZlmBEVt8GopT0f32N13qY7tYnM,402
17
17
  hydraflow/executor/io.py,sha256=yZMcBVmAbPZZ82cAXhgiJfj9p8WvHmzOCMBg_vtEVek,1509
18
- hydraflow/executor/job.py,sha256=Ug_4iVL-rNzMvVjBDecn3FVFGLmRzJEf4Pu6LP_x3LA,4844
18
+ hydraflow/executor/job.py,sha256=bmjlqE-cE7lyNFFKj1nUhxiQHKf7DsFkCSTD9iTEQ5I,5606
19
19
  hydraflow/executor/parser.py,sha256=_Rfund3FDgrXitTt_znsTpgEtMDqZ_ICynaB_Zje14Q,14561
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,,
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,,