hydraflow 0.7.5__py3-none-any.whl → 0.9.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/__init__.py +5 -20
- hydraflow/cli.py +31 -39
- hydraflow/core/__init__.py +0 -0
- hydraflow/{config.py → core/config.py} +10 -27
- hydraflow/{context.py → core/context.py} +8 -50
- hydraflow/{utils.py → core/io.py} +19 -28
- hydraflow/core/main.py +164 -0
- hydraflow/core/mlflow.py +168 -0
- hydraflow/{param.py → core/param.py} +2 -2
- hydraflow/entities/__init__.py +0 -0
- hydraflow/{run_collection.py → entities/run_collection.py} +18 -163
- hydraflow/{run_data.py → entities/run_data.py} +5 -3
- hydraflow/{run_info.py → entities/run_info.py} +2 -2
- hydraflow/executor/__init__.py +0 -0
- hydraflow/executor/conf.py +23 -0
- hydraflow/executor/io.py +34 -0
- hydraflow/executor/job.py +152 -0
- hydraflow/executor/parser.py +397 -0
- {hydraflow-0.7.5.dist-info → hydraflow-0.9.0.dist-info}/METADATA +18 -19
- hydraflow-0.9.0.dist-info/RECORD +24 -0
- hydraflow/main.py +0 -54
- hydraflow/mlflow.py +0 -280
- hydraflow-0.7.5.dist-info/RECORD +0 -17
- {hydraflow-0.7.5.dist-info → hydraflow-0.9.0.dist-info}/WHEEL +0 -0
- {hydraflow-0.7.5.dist-info → hydraflow-0.9.0.dist-info}/entry_points.txt +0 -0
- {hydraflow-0.7.5.dist-info → hydraflow-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,152 @@
|
|
1
|
+
"""Job execution and argument handling for HydraFlow.
|
2
|
+
|
3
|
+
This module provides functionality for executing jobs in HydraFlow, including:
|
4
|
+
|
5
|
+
- Argument parsing and expansion for job steps
|
6
|
+
- Batch processing of Hydra configurations
|
7
|
+
- Execution of jobs via shell commands or Python functions
|
8
|
+
|
9
|
+
The module supports two execution modes:
|
10
|
+
|
11
|
+
1. Shell command execution
|
12
|
+
2. Python function calls
|
13
|
+
|
14
|
+
Each job can consist of multiple steps, and each step can have its own
|
15
|
+
arguments and options that will be expanded into multiple runs.
|
16
|
+
"""
|
17
|
+
|
18
|
+
from __future__ import annotations
|
19
|
+
|
20
|
+
import importlib
|
21
|
+
import shlex
|
22
|
+
import subprocess
|
23
|
+
from subprocess import CalledProcessError
|
24
|
+
from typing import TYPE_CHECKING
|
25
|
+
|
26
|
+
import ulid
|
27
|
+
|
28
|
+
from .parser import collect, expand
|
29
|
+
|
30
|
+
if TYPE_CHECKING:
|
31
|
+
from collections.abc import Iterator
|
32
|
+
|
33
|
+
from .conf import Job, Step
|
34
|
+
|
35
|
+
|
36
|
+
def iter_args(step: Step) -> Iterator[list[str]]:
|
37
|
+
"""Iterate over combinations generated from parsed arguments.
|
38
|
+
|
39
|
+
Generate all possible combinations of arguments by parsing and
|
40
|
+
expanding each one, yielding them as an iterator.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
step (Step): The step to parse.
|
44
|
+
|
45
|
+
Yields:
|
46
|
+
list[str]: a list of the parsed argument combinations.
|
47
|
+
|
48
|
+
"""
|
49
|
+
args = collect(step.args)
|
50
|
+
options = [o for o in step.options.split(" ") if o]
|
51
|
+
|
52
|
+
for batch in expand(step.batch):
|
53
|
+
yield [*options, *sorted([*batch, *args])]
|
54
|
+
|
55
|
+
|
56
|
+
def iter_batches(job: Job) -> Iterator[list[str]]:
|
57
|
+
"""Generate Hydra application arguments for a job.
|
58
|
+
|
59
|
+
This function generates a list of Hydra application arguments
|
60
|
+
for a given job, including the job name and the root directory
|
61
|
+
for the sweep.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
job (Job): The job to generate the Hydra configuration for.
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
list[str]: A list of Hydra configuration strings.
|
68
|
+
|
69
|
+
"""
|
70
|
+
job_name = f"hydra.job.name={job.name}"
|
71
|
+
|
72
|
+
for step in job.steps:
|
73
|
+
for args in iter_args(step):
|
74
|
+
sweep_dir = f"hydra.sweep.dir=multirun/{ulid.ulid()}"
|
75
|
+
yield ["--multirun", sweep_dir, job_name, *args]
|
76
|
+
|
77
|
+
|
78
|
+
def multirun(job: Job) -> None:
|
79
|
+
"""Execute multiple runs of a job using either shell commands or Python functions.
|
80
|
+
|
81
|
+
This function processes a job configuration and executes it in one of two modes:
|
82
|
+
|
83
|
+
1. Shell command mode (job.run): Executes shell commands with the generated
|
84
|
+
arguments
|
85
|
+
2. Python function mode (job.call): Calls a Python function with the generated
|
86
|
+
arguments
|
87
|
+
|
88
|
+
Args:
|
89
|
+
job (Job): The job configuration containing run parameters and steps.
|
90
|
+
|
91
|
+
Raises:
|
92
|
+
RuntimeError: If a shell command fails or if a function call encounters
|
93
|
+
an error.
|
94
|
+
ValueError: If the Python function path is invalid or the function cannot
|
95
|
+
be imported.
|
96
|
+
|
97
|
+
"""
|
98
|
+
it = iter_batches(job)
|
99
|
+
|
100
|
+
if job.run:
|
101
|
+
base_cmds = shlex.split(job.run)
|
102
|
+
for args in it:
|
103
|
+
cmds = [*base_cmds, *args]
|
104
|
+
try:
|
105
|
+
subprocess.run(cmds, check=True)
|
106
|
+
except CalledProcessError as e:
|
107
|
+
msg = f"Command failed with exit code {e.returncode}"
|
108
|
+
raise RuntimeError(msg) from e
|
109
|
+
|
110
|
+
elif job.call:
|
111
|
+
if "." not in job.call:
|
112
|
+
msg = f"Invalid function path: {job.call}."
|
113
|
+
msg += " Expected format: 'package.module.function'"
|
114
|
+
raise ValueError(msg)
|
115
|
+
|
116
|
+
try:
|
117
|
+
module_name, func_name = job.call.rsplit(".", 1)
|
118
|
+
module = importlib.import_module(module_name)
|
119
|
+
func = getattr(module, func_name)
|
120
|
+
except (ImportError, AttributeError, ModuleNotFoundError) as e:
|
121
|
+
msg = f"Failed to import or find function: {job.call}"
|
122
|
+
raise ValueError(msg) from e
|
123
|
+
|
124
|
+
for args in it:
|
125
|
+
try:
|
126
|
+
func(*args)
|
127
|
+
except Exception as e: # noqa: PERF203
|
128
|
+
msg = f"Function call '{job.call}' failed with args: {args}"
|
129
|
+
raise RuntimeError(msg) from e
|
130
|
+
|
131
|
+
|
132
|
+
def show(job: Job) -> None:
|
133
|
+
"""Show the job configuration.
|
134
|
+
|
135
|
+
This function shows the job configuration for a given job.
|
136
|
+
|
137
|
+
Args:
|
138
|
+
job (Job): The job configuration to show.
|
139
|
+
|
140
|
+
"""
|
141
|
+
it = iter_batches(job)
|
142
|
+
|
143
|
+
if job.run:
|
144
|
+
base_cmds = shlex.split(job.run)
|
145
|
+
for args in it:
|
146
|
+
cmds = " ".join([*base_cmds, *args])
|
147
|
+
print(cmds) # noqa: T201
|
148
|
+
|
149
|
+
elif job.call:
|
150
|
+
print(f"call: {job.call}") # noqa: T201
|
151
|
+
for args in it:
|
152
|
+
print(f"args: {args}") # noqa: T201
|
@@ -0,0 +1,397 @@
|
|
1
|
+
"""Parse and convert string representations of numbers and ranges.
|
2
|
+
|
3
|
+
This module provides utility functions for parsing and converting
|
4
|
+
string representations of numbers and ranges. It includes functions
|
5
|
+
to convert strings to numbers, count decimal places, handle numeric
|
6
|
+
ranges, and expand values from string arguments.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from __future__ import annotations
|
10
|
+
|
11
|
+
import re
|
12
|
+
from itertools import chain, product
|
13
|
+
from typing import TYPE_CHECKING
|
14
|
+
|
15
|
+
if TYPE_CHECKING:
|
16
|
+
from collections.abc import Iterator
|
17
|
+
|
18
|
+
|
19
|
+
def to_number(x: str) -> int | float:
|
20
|
+
"""Convert a string to an integer or float.
|
21
|
+
|
22
|
+
Attempts to convert a string to an integer or a float,
|
23
|
+
returning 0 if the string is empty or cannot be converted.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
x (str): The string to convert.
|
27
|
+
|
28
|
+
Returns:
|
29
|
+
int | float: The converted number as an integer or float.
|
30
|
+
|
31
|
+
Examples:
|
32
|
+
>>> type(to_number("1"))
|
33
|
+
<class 'int'>
|
34
|
+
>>> type(to_number("1.2"))
|
35
|
+
<class 'float'>
|
36
|
+
>>> to_number("")
|
37
|
+
0
|
38
|
+
>>> to_number("1e-3")
|
39
|
+
0.001
|
40
|
+
|
41
|
+
"""
|
42
|
+
if not x:
|
43
|
+
return 0
|
44
|
+
|
45
|
+
if "." in x or "e" in x.lower():
|
46
|
+
return float(x)
|
47
|
+
|
48
|
+
return int(x)
|
49
|
+
|
50
|
+
|
51
|
+
def count_decimal_places(x: str) -> int:
|
52
|
+
"""Count decimal places in a string.
|
53
|
+
|
54
|
+
Examine a string representing a number and returns the count
|
55
|
+
of decimal places present after the decimal point.
|
56
|
+
Return 0 if no decimal point is found.
|
57
|
+
|
58
|
+
Args:
|
59
|
+
x (str): The string to check.
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
int: The number of decimal places.
|
63
|
+
|
64
|
+
Examples:
|
65
|
+
>>> count_decimal_places("1")
|
66
|
+
0
|
67
|
+
>>> count_decimal_places("-1.2")
|
68
|
+
1
|
69
|
+
>>> count_decimal_places("1.234")
|
70
|
+
3
|
71
|
+
>>> count_decimal_places("-1.234e-10")
|
72
|
+
3
|
73
|
+
|
74
|
+
"""
|
75
|
+
if "." not in x:
|
76
|
+
return 0
|
77
|
+
|
78
|
+
decimal_part = x.split(".")[1]
|
79
|
+
if "e" in decimal_part.lower():
|
80
|
+
decimal_part = decimal_part.split("e")[0]
|
81
|
+
|
82
|
+
return len(decimal_part)
|
83
|
+
|
84
|
+
|
85
|
+
def is_number(x: str) -> bool:
|
86
|
+
"""Check if a string is a number.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
x (str): The string to check.
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
bool: True if the string is a number, False otherwise.
|
93
|
+
|
94
|
+
Examples:
|
95
|
+
>>> is_number("1")
|
96
|
+
True
|
97
|
+
>>> is_number("-1.2")
|
98
|
+
True
|
99
|
+
>>> is_number("1.2.3")
|
100
|
+
False
|
101
|
+
|
102
|
+
"""
|
103
|
+
try:
|
104
|
+
float(x)
|
105
|
+
except ValueError:
|
106
|
+
return False
|
107
|
+
return True
|
108
|
+
|
109
|
+
|
110
|
+
SUFFIX_EXPONENT = {
|
111
|
+
"T": "e12",
|
112
|
+
"G": "e9",
|
113
|
+
"M": "e6",
|
114
|
+
"k": "e3",
|
115
|
+
"m": "e-3",
|
116
|
+
"u": "e-6",
|
117
|
+
"n": "e-9",
|
118
|
+
"p": "e-12",
|
119
|
+
"f": "e-15",
|
120
|
+
}
|
121
|
+
|
122
|
+
|
123
|
+
def _get_range(arg: str) -> tuple[float, float, float]:
|
124
|
+
args = [to_number(x) for x in arg.split(":")]
|
125
|
+
|
126
|
+
if len(args) == 2:
|
127
|
+
if args[0] > args[1]:
|
128
|
+
raise ValueError("start cannot be greater than stop")
|
129
|
+
|
130
|
+
return (args[0], 1, args[1])
|
131
|
+
|
132
|
+
if args[1] == 0:
|
133
|
+
raise ValueError("step cannot be zero")
|
134
|
+
if args[1] > 0 and args[0] > args[2]:
|
135
|
+
raise ValueError("start cannot be greater than stop")
|
136
|
+
if args[1] < 0 and args[0] < args[2]:
|
137
|
+
raise ValueError("start cannot be less than stop")
|
138
|
+
|
139
|
+
return args[0], args[1], args[2]
|
140
|
+
|
141
|
+
|
142
|
+
def _arange(start: float, step: float, stop: float) -> list[float]:
|
143
|
+
result = []
|
144
|
+
current = start
|
145
|
+
|
146
|
+
while current <= stop if step > 0 else current >= stop:
|
147
|
+
result.append(current)
|
148
|
+
current += step
|
149
|
+
|
150
|
+
return result
|
151
|
+
|
152
|
+
|
153
|
+
def split_suffix(arg: str) -> tuple[str, str]:
|
154
|
+
"""Split a string into prefix and suffix.
|
155
|
+
|
156
|
+
Args:
|
157
|
+
arg (str): The string to split.
|
158
|
+
|
159
|
+
Returns:
|
160
|
+
tuple[str, str]: A tuple containing the prefix and suffix.
|
161
|
+
|
162
|
+
Examples:
|
163
|
+
>>> split_suffix("1:k")
|
164
|
+
('1', 'e3')
|
165
|
+
>>> split_suffix("1:2:k")
|
166
|
+
('1:2', 'e3')
|
167
|
+
>>> split_suffix("1:2:M")
|
168
|
+
('1:2', 'e6')
|
169
|
+
>>> split_suffix(":1:2:M")
|
170
|
+
(':1:2', 'e6')
|
171
|
+
|
172
|
+
"""
|
173
|
+
if len(arg) < 3 or ":" not in arg:
|
174
|
+
return arg, ""
|
175
|
+
|
176
|
+
prefix, suffix = arg.rsplit(":", 1)
|
177
|
+
|
178
|
+
if suffix.lower().startswith("e"):
|
179
|
+
return prefix, suffix
|
180
|
+
|
181
|
+
if suffix not in SUFFIX_EXPONENT:
|
182
|
+
return arg, ""
|
183
|
+
|
184
|
+
return prefix, SUFFIX_EXPONENT[suffix]
|
185
|
+
|
186
|
+
|
187
|
+
def add_exponent(value: str, exponent: str) -> str:
|
188
|
+
"""Append an exponent to a value string.
|
189
|
+
|
190
|
+
Args:
|
191
|
+
value (str): The value to modify.
|
192
|
+
exponent (str): The exponent to append.
|
193
|
+
|
194
|
+
Returns:
|
195
|
+
str: The value with the exponent added.
|
196
|
+
|
197
|
+
"""
|
198
|
+
if value in ["0", "0.", "0.0"] or not exponent:
|
199
|
+
return value
|
200
|
+
|
201
|
+
return f"{value}{exponent}"
|
202
|
+
|
203
|
+
|
204
|
+
def collect_values(arg: str) -> list[str]:
|
205
|
+
"""Collect a list of values from a range argument.
|
206
|
+
|
207
|
+
Collect all individual values within a numeric range
|
208
|
+
represented by a string (e.g., `1:4`) and return them
|
209
|
+
as a list of strings.
|
210
|
+
Support both integer and floating-point ranges.
|
211
|
+
|
212
|
+
Args:
|
213
|
+
arg (str): The argument to collect.
|
214
|
+
|
215
|
+
Returns:
|
216
|
+
list[str]: A list of the collected values.
|
217
|
+
|
218
|
+
"""
|
219
|
+
if ":" not in arg:
|
220
|
+
return [arg]
|
221
|
+
|
222
|
+
arg, exponent = split_suffix(arg)
|
223
|
+
|
224
|
+
if ":" not in arg:
|
225
|
+
return [f"{arg}{exponent}"]
|
226
|
+
|
227
|
+
rng = _get_range(arg)
|
228
|
+
|
229
|
+
if all(isinstance(x, int) for x in rng):
|
230
|
+
values = [str(x) for x in _arange(*rng)]
|
231
|
+
else:
|
232
|
+
n = max(*(count_decimal_places(x) for x in arg.split(":")))
|
233
|
+
values = [str(round(x, n)) for x in _arange(*rng)]
|
234
|
+
|
235
|
+
return [add_exponent(x, exponent) for x in values]
|
236
|
+
|
237
|
+
|
238
|
+
def split(arg: str) -> list[str]:
|
239
|
+
r"""Split a string by top-level commas.
|
240
|
+
|
241
|
+
Splits a string by commas while respecting nested structures.
|
242
|
+
Commas inside brackets and quotes are ignored, only splitting
|
243
|
+
at the top-level commas.
|
244
|
+
|
245
|
+
Args:
|
246
|
+
arg (str): The string to split.
|
247
|
+
|
248
|
+
Returns:
|
249
|
+
list[str]: A list of split strings.
|
250
|
+
|
251
|
+
Examples:
|
252
|
+
>>> split("[a,1],[b,2]")
|
253
|
+
['[a,1]', '[b,2]']
|
254
|
+
>>> split('"x,y",z')
|
255
|
+
['"x,y"', 'z']
|
256
|
+
>>> split("'p,q',r")
|
257
|
+
["'p,q'", 'r']
|
258
|
+
|
259
|
+
"""
|
260
|
+
result = []
|
261
|
+
current = []
|
262
|
+
bracket_count = 0
|
263
|
+
in_single_quote = False
|
264
|
+
in_double_quote = False
|
265
|
+
|
266
|
+
for char in arg:
|
267
|
+
if char == "'" and not in_double_quote:
|
268
|
+
in_single_quote = not in_single_quote
|
269
|
+
elif char == '"' and not in_single_quote:
|
270
|
+
in_double_quote = not in_double_quote
|
271
|
+
elif char == "[" and not (in_single_quote or in_double_quote):
|
272
|
+
bracket_count += 1
|
273
|
+
elif char == "]" and not (in_single_quote or in_double_quote):
|
274
|
+
bracket_count -= 1
|
275
|
+
elif (
|
276
|
+
char == ","
|
277
|
+
and bracket_count == 0
|
278
|
+
and not in_single_quote
|
279
|
+
and not in_double_quote
|
280
|
+
):
|
281
|
+
result.append("".join(current))
|
282
|
+
current = []
|
283
|
+
continue
|
284
|
+
current.append(char)
|
285
|
+
|
286
|
+
if current:
|
287
|
+
result.append("".join(current))
|
288
|
+
|
289
|
+
return result
|
290
|
+
|
291
|
+
|
292
|
+
def expand_values(arg: str) -> list[str]:
|
293
|
+
"""Expand a string argument into a list of values.
|
294
|
+
|
295
|
+
Take a string containing comma-separated values or ranges and return a list
|
296
|
+
of all individual values. Handle numeric ranges and special characters.
|
297
|
+
|
298
|
+
Args:
|
299
|
+
arg (str): The argument to expand.
|
300
|
+
|
301
|
+
Returns:
|
302
|
+
list[str]: A list of the expanded values.
|
303
|
+
|
304
|
+
"""
|
305
|
+
return list(chain.from_iterable(collect_values(x) for x in split(arg)))
|
306
|
+
|
307
|
+
|
308
|
+
def collect_arg(arg: str) -> str:
|
309
|
+
"""Collect a string of expanded key-value pairs.
|
310
|
+
|
311
|
+
Take a key-value pair argument and concatenates all expanded values with commas,
|
312
|
+
returning a single string suitable for command-line usage.
|
313
|
+
|
314
|
+
Args:
|
315
|
+
arg (str): The argument to collect.
|
316
|
+
|
317
|
+
Returns:
|
318
|
+
str: A string of the collected key and values.
|
319
|
+
|
320
|
+
"""
|
321
|
+
key, arg = arg.split("=")
|
322
|
+
arg = ",".join(expand_values(arg))
|
323
|
+
return f"{key}={arg}"
|
324
|
+
|
325
|
+
|
326
|
+
def expand_arg(arg: str) -> Iterator[str]:
|
327
|
+
"""Parse a string argument into a list of values.
|
328
|
+
|
329
|
+
Responsible for parsing a string that may contain multiple
|
330
|
+
arguments separated by pipes ("|") and returns a list of all
|
331
|
+
expanded arguments.
|
332
|
+
|
333
|
+
Args:
|
334
|
+
arg (str): The argument to parse.
|
335
|
+
|
336
|
+
Returns:
|
337
|
+
list[str]: A list of the parsed arguments.
|
338
|
+
|
339
|
+
"""
|
340
|
+
if "|" not in arg:
|
341
|
+
key, value = arg.split("=")
|
342
|
+
|
343
|
+
for v in expand_values(value):
|
344
|
+
yield f"{key}={v}"
|
345
|
+
|
346
|
+
return
|
347
|
+
|
348
|
+
args = arg.split("|")
|
349
|
+
key = ""
|
350
|
+
|
351
|
+
for arg_ in args:
|
352
|
+
if "=" in arg_:
|
353
|
+
key, value = arg_.split("=")
|
354
|
+
elif key:
|
355
|
+
value = arg_
|
356
|
+
else:
|
357
|
+
msg = f"Invalid argument: {arg_}"
|
358
|
+
raise ValueError(msg)
|
359
|
+
|
360
|
+
value = ",".join(expand_values(value))
|
361
|
+
yield f"{key}={value}"
|
362
|
+
|
363
|
+
|
364
|
+
def collect(args: str | list[str]) -> list[str]:
|
365
|
+
"""Collect a list of arguments into a list of strings.
|
366
|
+
|
367
|
+
Args:
|
368
|
+
args (list[str]): The arguments to collect.
|
369
|
+
|
370
|
+
Returns:
|
371
|
+
list[str]: A list of the collected arguments.
|
372
|
+
|
373
|
+
"""
|
374
|
+
if isinstance(args, str):
|
375
|
+
args = re.split(r"\s+", args.strip())
|
376
|
+
|
377
|
+
args = [arg for arg in args if "=" in arg]
|
378
|
+
|
379
|
+
return [collect_arg(arg) for arg in args]
|
380
|
+
|
381
|
+
|
382
|
+
def expand(args: str | list[str]) -> list[list[str]]:
|
383
|
+
"""Expand a list of arguments into a list of lists of strings.
|
384
|
+
|
385
|
+
Args:
|
386
|
+
args (list[str]): The arguments to expand.
|
387
|
+
|
388
|
+
Returns:
|
389
|
+
list[list[str]]: A list of the expanded arguments.
|
390
|
+
|
391
|
+
"""
|
392
|
+
if isinstance(args, str):
|
393
|
+
args = re.split(r"\s+", args.strip())
|
394
|
+
|
395
|
+
args = [arg for arg in args if "=" in arg]
|
396
|
+
|
397
|
+
return [list(x) for x in product(*(expand_arg(arg) for arg in args))]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hydraflow
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.9.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
|
@@ -41,6 +41,7 @@ Requires-Dist: mlflow>=2.15
|
|
41
41
|
Requires-Dist: omegaconf
|
42
42
|
Requires-Dist: rich
|
43
43
|
Requires-Dist: typer
|
44
|
+
Requires-Dist: ulid
|
44
45
|
Description-Content-Type: text/markdown
|
45
46
|
|
46
47
|
# Hydraflow
|
@@ -93,31 +94,29 @@ pip install hydraflow
|
|
93
94
|
Here is a simple example to get you started with Hydraflow:
|
94
95
|
|
95
96
|
```python
|
96
|
-
import
|
97
|
-
|
98
|
-
import mlflow
|
97
|
+
from __future__ import annotations
|
98
|
+
|
99
99
|
from dataclasses import dataclass
|
100
|
-
from hydra.core.config_store import ConfigStore
|
101
100
|
from pathlib import Path
|
101
|
+
from typing import TYPE_CHECKING
|
102
102
|
|
103
|
-
|
104
|
-
class MySQLConfig:
|
105
|
-
host: str = "localhost"
|
106
|
-
port: int = 3306
|
103
|
+
import hydraflow
|
107
104
|
|
108
|
-
|
109
|
-
|
105
|
+
if TYPE_CHECKING:
|
106
|
+
from mlflow.entities import Run
|
107
|
+
|
108
|
+
|
109
|
+
@dataclass
|
110
|
+
class Config:
|
111
|
+
count: int = 1
|
112
|
+
name: str = "a"
|
110
113
|
|
111
|
-
@hydra.main(version_base=None, config_name="config")
|
112
|
-
def my_app(cfg: MySQLConfig) -> None:
|
113
|
-
# Set experiment by Hydra job name.
|
114
|
-
hydraflow.set_experiment()
|
115
114
|
|
116
|
-
|
117
|
-
|
118
|
-
|
115
|
+
@hydraflow.main(Config)
|
116
|
+
def app(run: Run, cfg: Config):
|
117
|
+
"""Your app code here."""
|
119
118
|
|
120
119
|
|
121
120
|
if __name__ == "__main__":
|
122
|
-
|
121
|
+
app()
|
123
122
|
```
|
@@ -0,0 +1,24 @@
|
|
1
|
+
hydraflow/__init__.py,sha256=WnReG-E2paQSMLJm42N-KaQyHYsfA4OX-WWcst610PI,738
|
2
|
+
hydraflow/cli.py,sha256=gbDPj49azP8CCGxkxU0rksh1-gCyjP0VkVYH34ktcsA,1338
|
3
|
+
hydraflow/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
+
hydraflow/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
+
hydraflow/core/config.py,sha256=SJzjgsO_kzB78_whJ3lmy7GlZvTvwZONH1BJBn8zCuI,3817
|
6
|
+
hydraflow/core/context.py,sha256=QPyPg1xrTlmhviKNn-0nDY9bXcVky1zInqRqPN-VNhc,4741
|
7
|
+
hydraflow/core/io.py,sha256=T4ESiepEcqR-FZlo_m7VTBEFMwalrqPI8eFKPagvv3Q,4402
|
8
|
+
hydraflow/core/main.py,sha256=gYb1OOVH0CL4385Dm-06Mqi1Mr9-24URwLUiW86pGNs,5018
|
9
|
+
hydraflow/core/mlflow.py,sha256=M3MhiChnMzKnKRmjBl4h_SRGkAZKL7GAmFr3DdzwRuQ,5666
|
10
|
+
hydraflow/core/param.py,sha256=LHU9j9_7oA99igasoOyKofKClVr9FmGA3UABJ-KmyS0,4538
|
11
|
+
hydraflow/entities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
12
|
+
hydraflow/entities/run_collection.py,sha256=BHbMvYB4WyG5qSNYpIzwmHySOtmGNb-jg2OrCaTPr2I,19651
|
13
|
+
hydraflow/entities/run_data.py,sha256=Y2_Lc-BdQ7nXhcEIjdHGHIkLrXsmAktOftESEwYOY8o,1602
|
14
|
+
hydraflow/entities/run_info.py,sha256=FRC6ICOlzB2u_xi_33Qs-YZLt677UotuNbYqI7XSmHY,1017
|
15
|
+
hydraflow/executor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
|
+
hydraflow/executor/conf.py,sha256=q_FrPXQJCVGKS1FYnGRGqTUgMQeMBkaVPW2mtQc8oxk,384
|
17
|
+
hydraflow/executor/io.py,sha256=4nafwge6vHanYFuEHxd0LRv_3ZLgMpV50qSbssZNe3Q,696
|
18
|
+
hydraflow/executor/job.py,sha256=Vp2IZOuIC25Gqo9A_5MkllWl1T1QBfUnI5ksMvKwakg,4479
|
19
|
+
hydraflow/executor/parser.py,sha256=y4C9wVdUnazJDxdWrT5y3yWFIo0zAGzO-cS9x1MTK_8,9486
|
20
|
+
hydraflow-0.9.0.dist-info/METADATA,sha256=p8-vmPIYV-uU9BJbSB1roqrhNO7nK2HRdrpj8Tip1AA,4559
|
21
|
+
hydraflow-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
22
|
+
hydraflow-0.9.0.dist-info/entry_points.txt,sha256=XI0khPbpCIUo9UPqkNEpgh-kqK3Jy8T7L2VCWOdkbSM,48
|
23
|
+
hydraflow-0.9.0.dist-info/licenses/LICENSE,sha256=IGdDrBPqz1O0v_UwCW-NJlbX9Hy9b3uJ11t28y2srmY,1062
|
24
|
+
hydraflow-0.9.0.dist-info/RECORD,,
|
hydraflow/main.py
DELETED
@@ -1,54 +0,0 @@
|
|
1
|
-
"""main decorator."""
|
2
|
-
|
3
|
-
from __future__ import annotations
|
4
|
-
|
5
|
-
from functools import wraps
|
6
|
-
from typing import TYPE_CHECKING, Any
|
7
|
-
|
8
|
-
import hydra
|
9
|
-
from hydra.core.config_store import ConfigStore
|
10
|
-
from mlflow.entities import RunStatus
|
11
|
-
|
12
|
-
import hydraflow
|
13
|
-
|
14
|
-
if TYPE_CHECKING:
|
15
|
-
from collections.abc import Callable
|
16
|
-
|
17
|
-
from mlflow.entities import Run
|
18
|
-
|
19
|
-
FINISHED = RunStatus.to_string(RunStatus.FINISHED)
|
20
|
-
|
21
|
-
|
22
|
-
def main(
|
23
|
-
node: Any,
|
24
|
-
config_name: str = "config",
|
25
|
-
*,
|
26
|
-
chdir: bool = False,
|
27
|
-
force_new_run: bool = False,
|
28
|
-
skip_finished: bool = True,
|
29
|
-
):
|
30
|
-
"""Main decorator."""
|
31
|
-
|
32
|
-
def decorator(app: Callable[[Run, Any], None]) -> Callable[[], None]:
|
33
|
-
ConfigStore.instance().store(name=config_name, node=node)
|
34
|
-
|
35
|
-
@wraps(app)
|
36
|
-
@hydra.main(version_base=None, config_name=config_name)
|
37
|
-
def inner_app(cfg: object) -> None:
|
38
|
-
hydraflow.set_experiment()
|
39
|
-
|
40
|
-
if force_new_run:
|
41
|
-
run = None
|
42
|
-
else:
|
43
|
-
rc = hydraflow.search_runs()
|
44
|
-
run = rc.try_get(cfg, override=True)
|
45
|
-
|
46
|
-
if skip_finished and run and run.info.status == FINISHED:
|
47
|
-
return
|
48
|
-
|
49
|
-
with hydraflow.start_run(cfg, run=run, chdir=chdir) as run:
|
50
|
-
app(run, cfg)
|
51
|
-
|
52
|
-
return inner_app
|
53
|
-
|
54
|
-
return decorator
|