hydraflow 0.12.4__py3-none-any.whl → 0.12.5__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
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import shlex
5
6
  from typing import Annotated
6
7
 
7
8
  import typer
@@ -24,7 +25,13 @@ def run(
24
25
  """Run a job."""
25
26
 
26
27
  from hydraflow.executor.io import get_job
27
- from hydraflow.executor.job import multirun, to_text
28
+ from hydraflow.executor.job import (
29
+ iter_batches,
30
+ iter_calls,
31
+ iter_runs,
32
+ submit,
33
+ to_text,
34
+ )
28
35
 
29
36
  job = get_job(name)
30
37
 
@@ -35,7 +42,24 @@ def run(
35
42
  import mlflow
36
43
 
37
44
  mlflow.set_experiment(job.name)
38
- multirun(job)
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
+ if job.run:
52
+ executable, *args = shlex.split(job.run)
53
+ it = iter_runs(executable, args, iter_batches(job))
54
+ elif job.call:
55
+ funcname, *args = shlex.split(job.call)
56
+ it = iter_calls(funcname, args, iter_batches(job))
57
+ else:
58
+ typer.echo(f"No command found in job: {job.name}.")
59
+ raise typer.Exit(1)
60
+
61
+ for _ in it:
62
+ pass
39
63
 
40
64
 
41
65
  @app.command()
hydraflow/core/io.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import fnmatch
5
6
  import shutil
6
7
  import urllib.parse
7
8
  import urllib.request
@@ -152,21 +153,6 @@ def remove_run(run: Run | Iterable[Run]) -> None:
152
153
  shutil.rmtree(get_artifact_dir(run).parent)
153
154
 
154
155
 
155
- def get_root_dir(uri: str | Path | None = None) -> Path:
156
- """Get the root directory for the MLflow tracking server."""
157
- import mlflow
158
-
159
- if uri is not None:
160
- return Path(uri).absolute()
161
-
162
- uri = mlflow.get_tracking_uri()
163
-
164
- if uri.startswith("file:"):
165
- return file_uri_to_path(uri)
166
-
167
- return Path(uri).absolute()
168
-
169
-
170
156
  def get_experiment_name(path: Path) -> str | None:
171
157
  """Get the experiment name from the meta file."""
172
158
  metafile = path / "meta.yaml"
@@ -195,50 +181,49 @@ def predicate_experiment_dir(
195
181
  return True
196
182
 
197
183
  if isinstance(experiment_names, list):
198
- return name in experiment_names
184
+ return any(fnmatch.fnmatch(name, e) for e in experiment_names)
199
185
 
200
186
  return experiment_names(name)
201
187
 
202
188
 
203
189
  def iter_experiment_dirs(
190
+ root_dir: str | Path,
204
191
  experiment_names: str | list[str] | Callable[[str], bool] | None = None,
205
- root_dir: str | Path | None = None,
206
192
  ) -> Iterator[Path]:
207
193
  """Iterate over the experiment directories in the root directory."""
208
194
  if isinstance(experiment_names, str):
209
195
  experiment_names = [experiment_names]
210
196
 
211
- root_dir = get_root_dir(root_dir)
212
- for path in root_dir.iterdir():
197
+ for path in Path(root_dir).iterdir():
213
198
  if predicate_experiment_dir(path, experiment_names):
214
199
  yield path
215
200
 
216
201
 
217
202
  def iter_run_dirs(
203
+ root_dir: str | Path,
218
204
  experiment_names: str | list[str] | Callable[[str], bool] | None = None,
219
- root_dir: str | Path | None = None,
220
205
  ) -> Iterator[Path]:
221
206
  """Iterate over the run directories in the root directory."""
222
- for experiment_dir in iter_experiment_dirs(experiment_names, root_dir):
207
+ for experiment_dir in iter_experiment_dirs(root_dir, experiment_names):
223
208
  for path in experiment_dir.iterdir():
224
209
  if path.is_dir() and (path / "artifacts").exists():
225
210
  yield path
226
211
 
227
212
 
228
213
  def iter_artifacts_dirs(
214
+ root_dir: str | Path,
229
215
  experiment_names: str | list[str] | Callable[[str], bool] | None = None,
230
- root_dir: str | Path | None = None,
231
216
  ) -> Iterator[Path]:
232
217
  """Iterate over the artifacts directories in the root directory."""
233
- for path in iter_run_dirs(experiment_names, root_dir):
218
+ for path in iter_run_dirs(root_dir, experiment_names):
234
219
  yield path / "artifacts"
235
220
 
236
221
 
237
222
  def iter_artifact_paths(
223
+ root_dir: str | Path,
238
224
  artifact_path: str | Path,
239
225
  experiment_names: str | list[str] | Callable[[str], bool] | None = None,
240
- root_dir: str | Path | None = None,
241
226
  ) -> Iterator[Path]:
242
227
  """Iterate over the artifact paths in the root directory."""
243
- for path in iter_artifacts_dirs(experiment_names, root_dir):
228
+ for path in iter_artifacts_dirs(root_dir, experiment_names):
244
229
  yield path / artifact_path
@@ -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
@@ -21,7 +21,7 @@ import importlib
21
21
  import shlex
22
22
  import subprocess
23
23
  import sys
24
- from subprocess import CalledProcessError
24
+ from dataclasses import dataclass
25
25
  from typing import TYPE_CHECKING
26
26
 
27
27
  import ulid
@@ -29,7 +29,9 @@ import ulid
29
29
  from .parser import collect, expand
30
30
 
31
31
  if TYPE_CHECKING:
32
- from collections.abc import Iterator
32
+ from collections.abc import Callable, Iterable, Iterator
33
+ from subprocess import CompletedProcess
34
+ from typing import Any
33
35
 
34
36
  from .conf import Job
35
37
 
@@ -79,63 +81,81 @@ def iter_batches(job: Job) -> Iterator[list[str]]:
79
81
  yield ["--multirun", *args, job_name, sweep_dir, *configs]
80
82
 
81
83
 
82
- def multirun(job: Job) -> None:
83
- """Execute multiple runs of a job using either shell commands or Python functions.
84
+ @dataclass
85
+ class Run:
86
+ """An executed run."""
84
87
 
85
- This function processes a job configuration and executes it in one of two modes:
88
+ total: int
89
+ completed: int
90
+ result: CompletedProcess
86
91
 
87
- 1. Shell command mode (job.run): Executes shell commands with the generated
88
- arguments
89
- 2. Python function mode (job.call): Calls a Python function with the generated
90
- arguments
91
92
 
92
- Args:
93
- job (Job): The job configuration containing run parameters and steps.
93
+ @dataclass
94
+ class Call:
95
+ """An executed call."""
94
96
 
95
- Raises:
96
- RuntimeError: If a shell command fails or if a function call encounters
97
- an error.
98
- ValueError: If the Python function path is invalid or the function cannot
99
- be imported.
97
+ total: int
98
+ completed: int
99
+ result: Any
100
100
 
101
- """
102
- it = iter_batches(job)
103
101
 
104
- if job.run:
105
- base_cmds = shlex.split(job.run)
106
- if base_cmds[0] == "python" and sys.platform == "win32":
107
- base_cmds[0] = sys.executable
102
+ def iter_runs(
103
+ executable: str,
104
+ args: list[str],
105
+ iterable: Iterable[list[str]],
106
+ ) -> Iterator[Run]:
107
+ """Execute multiple runs of a job using shell commands."""
108
+ if executable == "python" and sys.platform == "win32":
109
+ executable = sys.executable
108
110
 
109
- for args in it:
110
- cmds = [*base_cmds, *args]
111
- try:
112
- subprocess.run(cmds, check=True)
113
- except CalledProcessError as e:
114
- msg = f"Command failed with exit code {e.returncode}"
115
- raise RuntimeError(msg) from e
111
+ iterable = list(iterable)
112
+ total = len(iterable)
116
113
 
117
- elif job.call:
118
- call_name, *base_args = shlex.split(job.call)
114
+ for completed, args_ in enumerate(iterable, 1):
115
+ result = subprocess.run([executable, *args, *args_], check=False)
116
+ yield Run(total, completed, result)
119
117
 
120
- if "." not in call_name:
121
- msg = f"Invalid function path: {call_name}."
122
- msg += " Expected format: 'package.module.function'"
123
- raise ValueError(msg)
124
118
 
125
- try:
126
- module_name, func_name = call_name.rsplit(".", 1)
127
- module = importlib.import_module(module_name)
128
- func = getattr(module, func_name)
129
- except (ImportError, AttributeError, ModuleNotFoundError) as e:
130
- msg = f"Failed to import or find function: {call_name}"
131
- raise ValueError(msg) from e
119
+ def iter_calls(
120
+ funcname: str,
121
+ args: list[str],
122
+ iterable: Iterable[list[str]],
123
+ ) -> Iterator[Call]:
124
+ """Execute multiple calls of a job using Python functions."""
125
+ func = get_callable(funcname)
132
126
 
133
- for args in it:
134
- try:
135
- func([*base_args, *args])
136
- except Exception as e: # noqa: PERF203
137
- msg = f"Function call '{job.call}' failed with args: {args}"
138
- raise RuntimeError(msg) from e
127
+ iterable = list(iterable)
128
+ total = len(iterable)
129
+
130
+ for completed, args_ in enumerate(iterable, 1):
131
+ result = func([*args, *args_])
132
+ yield Call(total, completed, result)
133
+
134
+
135
+ def submit(
136
+ funcname: str,
137
+ args: list[str],
138
+ 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])
143
+
144
+
145
+ def get_callable(name: str) -> Callable:
146
+ """Get a callable from a function name."""
147
+ if "." not in name:
148
+ msg = f"Invalid function path: {name}."
149
+ raise ValueError(msg)
150
+
151
+ try:
152
+ module_name, func_name = name.rsplit(".", 1)
153
+ module = importlib.import_module(module_name)
154
+ return getattr(module, func_name)
155
+
156
+ except (ImportError, AttributeError, ModuleNotFoundError) as e:
157
+ msg = f"Failed to import or find function: {name}"
158
+ raise ValueError(msg) from e
139
159
 
140
160
 
141
161
  def to_text(job: Job) -> str:
@@ -165,4 +185,9 @@ def to_text(job: Job) -> str:
165
185
  for args in it:
166
186
  text += f"args: {args}\n"
167
187
 
188
+ elif job.submit:
189
+ text = f"submit: {job.submit}\n"
190
+ for args in it:
191
+ text += f"args: {args}\n"
192
+
168
193
  return text.rstrip()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hydraflow
3
- Version: 0.12.4
3
+ Version: 0.12.5
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,10 +1,10 @@
1
1
  hydraflow/__init__.py,sha256=f2KO2iF7um-nNmayNyEr7TWG4UICOXy7YAN1d3qu0OY,936
2
- hydraflow/cli.py,sha256=clQ7PD_okuGhbrgQ0q8Ldbb8xY3u3He5hI_uBI4h1Q4,1470
2
+ hydraflow/cli.py,sha256=Z9gpuxW6sgGg8lO5nmDE7dToLnNuWVT4nk9j0opqqHs,2075
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
6
6
  hydraflow/core/context.py,sha256=L4OygMLbITwlWzq17Lh8VoXKKtjOJ3DBEVsBddKPSJ8,4741
7
- hydraflow/core/io.py,sha256=6ZQYqxPUkIinFYqSQXJPzTSnuhfP1KfFchTNeNn-g6A,7311
7
+ hydraflow/core/io.py,sha256=Tch85xbdRao7rG9BMbRpc2Cq0glC8a8M87QDoyQ81p8,6926
8
8
  hydraflow/core/main.py,sha256=dY8uUykS_AbzverrSWkXLyj98TjBPHAiMUf_l5met1U,5162
9
9
  hydraflow/core/mlflow.py,sha256=OQJ3f2wkHJRb11ZK__HF4R8FyBEje7-NOqObpoanGhU,5704
10
10
  hydraflow/core/param.py,sha256=LHU9j9_7oA99igasoOyKofKClVr9FmGA3UABJ-KmyS0,4538
@@ -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=2dv6_PlsynRmia-fGZlmBEVt8GopT0f32N13qY7tYnM,402
16
+ hydraflow/executor/conf.py,sha256=icGbLDh86KgkyiGXwDoEkmZpgAP3X8Jmu_PYqJoTooY,423
17
17
  hydraflow/executor/io.py,sha256=yZMcBVmAbPZZ82cAXhgiJfj9p8WvHmzOCMBg_vtEVek,1509
18
- hydraflow/executor/job.py,sha256=IL7ek0Vwa3Bl_gANq0wCbldNCUclo8YBckeEeO6W6xg,4852
18
+ hydraflow/executor/job.py,sha256=Ug_4iVL-rNzMvVjBDecn3FVFGLmRzJEf4Pu6LP_x3LA,4844
19
19
  hydraflow/executor/parser.py,sha256=_Rfund3FDgrXitTt_znsTpgEtMDqZ_ICynaB_Zje14Q,14561
20
- hydraflow-0.12.4.dist-info/METADATA,sha256=CT9bb--1HhyO_tcahfCTzSUBH2nWt7i2lY2pS3Dac38,4549
21
- hydraflow-0.12.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
22
- hydraflow-0.12.4.dist-info/entry_points.txt,sha256=XI0khPbpCIUo9UPqkNEpgh-kqK3Jy8T7L2VCWOdkbSM,48
23
- hydraflow-0.12.4.dist-info/licenses/LICENSE,sha256=IGdDrBPqz1O0v_UwCW-NJlbX9Hy9b3uJ11t28y2srmY,1062
24
- hydraflow-0.12.4.dist-info/RECORD,,
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,,