hydraflow 0.8.0__tar.gz → 0.9.0__tar.gz
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-0.8.0 → hydraflow-0.9.0}/PKG-INFO +18 -19
- {hydraflow-0.8.0 → hydraflow-0.9.0}/README.md +16 -18
- {hydraflow-0.8.0 → hydraflow-0.9.0}/docs/usage/quickstart.md +1 -23
- {hydraflow-0.8.0 → hydraflow-0.9.0}/pyproject.toml +15 -4
- {hydraflow-0.8.0 → hydraflow-0.9.0}/src/hydraflow/__init__.py +5 -5
- hydraflow-0.9.0/src/hydraflow/cli.py +67 -0
- {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.0/src/hydraflow/core}/context.py +3 -2
- {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.0/src/hydraflow/core}/main.py +5 -3
- {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.0/src/hydraflow/core}/mlflow.py +4 -3
- {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.0/src/hydraflow/entities}/run_collection.py +8 -7
- {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.0/src/hydraflow/entities}/run_data.py +3 -3
- {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.0/src/hydraflow/entities}/run_info.py +2 -2
- hydraflow-0.9.0/src/hydraflow/executor/conf.py +23 -0
- hydraflow-0.9.0/src/hydraflow/executor/io.py +34 -0
- hydraflow-0.9.0/src/hydraflow/executor/job.py +152 -0
- hydraflow-0.9.0/src/hydraflow/executor/parser.py +397 -0
- hydraflow-0.9.0/tests/cli/app.py +24 -0
- hydraflow-0.9.0/tests/cli/conftest.py +12 -0
- hydraflow-0.9.0/tests/cli/hydraflow.yaml +13 -0
- hydraflow-0.9.0/tests/cli/test_run.py +33 -0
- hydraflow-0.9.0/tests/cli/test_setup.py +7 -0
- hydraflow-0.9.0/tests/cli/test_show.py +23 -0
- {hydraflow-0.8.0 → hydraflow-0.9.0}/tests/conftest.py +10 -5
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/config/test_config.py +2 -2
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/config/test_params.py +1 -1
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/context/test_chdir.py +6 -3
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/context/test_log_run.py +6 -3
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/context/test_start_run.py +6 -3
- hydraflow-0.9.0/tests/core/io/__init__.py +0 -0
- hydraflow-0.8.0/tests/utils/test_utils.py → hydraflow-0.9.0/tests/core/io/test_hydra_dir.py +10 -12
- {hydraflow-0.8.0/tests/utils → hydraflow-0.9.0/tests/core/io}/test_run.py +4 -4
- hydraflow-0.9.0/tests/core/main/__init__.py +0 -0
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/test_default.py +7 -6
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/test_force_new_run.py +6 -3
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/test_match_overrides.py +7 -4
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/test_rerun_finished.py +6 -3
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/test_skip_finished.py +8 -6
- hydraflow-0.9.0/tests/core/param/__init__.py +0 -0
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/param/test_param.py +6 -6
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/param/test_params.py +10 -7
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/test_mlflow.py +5 -5
- hydraflow-0.9.0/tests/entities/__init__.py +0 -0
- {hydraflow-0.8.0/tests/run → hydraflow-0.9.0/tests/entities}/test_collection.py +5 -5
- {hydraflow-0.8.0/tests/run → hydraflow-0.9.0/tests/entities}/test_data.py +2 -2
- {hydraflow-0.8.0/tests/run → hydraflow-0.9.0/tests/entities}/test_filter.py +6 -3
- {hydraflow-0.8.0/tests/run → hydraflow-0.9.0/tests/entities}/test_info.py +2 -2
- {hydraflow-0.8.0/tests/run → hydraflow-0.9.0/tests/entities}/test_values.py +5 -2
- hydraflow-0.9.0/tests/executor/__init__.py +0 -0
- hydraflow-0.9.0/tests/executor/conftest.py +30 -0
- hydraflow-0.9.0/tests/executor/echo.py +17 -0
- hydraflow-0.9.0/tests/executor/test_args.py +19 -0
- hydraflow-0.9.0/tests/executor/test_conf.py +34 -0
- hydraflow-0.9.0/tests/executor/test_io.py +18 -0
- hydraflow-0.9.0/tests/executor/test_job.py +127 -0
- hydraflow-0.9.0/tests/executor/test_parser.py +220 -0
- hydraflow-0.8.0/hydraflow.yaml +0 -5
- hydraflow-0.8.0/src/hydraflow/cli.py +0 -75
- hydraflow-0.8.0/tests/cli/conftest.py +0 -9
- hydraflow-0.8.0/tests/cli/test_run.py +0 -18
- hydraflow-0.8.0/tests/cli/test_show.py +0 -52
- {hydraflow-0.8.0 → hydraflow-0.9.0}/.devcontainer/devcontainer.json +0 -0
- {hydraflow-0.8.0 → hydraflow-0.9.0}/.devcontainer/postCreate.sh +0 -0
- {hydraflow-0.8.0 → hydraflow-0.9.0}/.devcontainer/starship.toml +0 -0
- {hydraflow-0.8.0 → hydraflow-0.9.0}/.gitattributes +0 -0
- {hydraflow-0.8.0 → hydraflow-0.9.0}/.github/workflows/ci.yaml +0 -0
- {hydraflow-0.8.0 → hydraflow-0.9.0}/.github/workflows/docs.yaml +0 -0
- {hydraflow-0.8.0 → hydraflow-0.9.0}/.gitignore +0 -0
- {hydraflow-0.8.0 → hydraflow-0.9.0}/LICENSE +0 -0
- {hydraflow-0.8.0 → hydraflow-0.9.0}/apps/quickstart.py +0 -0
- {hydraflow-0.8.0 → hydraflow-0.9.0}/docs/index.md +0 -0
- {hydraflow-0.8.0 → hydraflow-0.9.0}/mkdocs.yaml +0 -0
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/src/hydraflow/core}/__init__.py +0 -0
- {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.0/src/hydraflow/core}/config.py +0 -0
- /hydraflow-0.8.0/src/hydraflow/utils.py → /hydraflow-0.9.0/src/hydraflow/core/io.py +0 -0
- {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.0/src/hydraflow/core}/param.py +0 -0
- {hydraflow-0.8.0/tests/cli → hydraflow-0.9.0/src/hydraflow/entities}/__init__.py +0 -0
- {hydraflow-0.8.0/tests/config → hydraflow-0.9.0/src/hydraflow/executor}/__init__.py +0 -0
- {hydraflow-0.8.0 → hydraflow-0.9.0}/src/hydraflow/py.typed +0 -0
- {hydraflow-0.8.0/tests/context → hydraflow-0.9.0/tests}/__init__.py +0 -0
- {hydraflow-0.8.0/tests/main → hydraflow-0.9.0/tests/cli}/__init__.py +0 -0
- {hydraflow-0.8.0 → hydraflow-0.9.0}/tests/cli/test_version.py +0 -0
- {hydraflow-0.8.0/tests/param → hydraflow-0.9.0/tests/core}/__init__.py +0 -0
- {hydraflow-0.8.0/tests/run → hydraflow-0.9.0/tests/core/config}/__init__.py +0 -0
- {hydraflow-0.8.0/tests/utils → hydraflow-0.9.0/tests/core/context}/__init__.py +0 -0
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/context/chdir.py +0 -0
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/context/log_run.py +0 -0
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/context/start_run.py +0 -0
- /hydraflow-0.8.0/tests/utils/utils.py → /hydraflow-0.9.0/tests/core/io/hydra_dir.py +0 -0
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/default.py +0 -0
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/force_new_run.py +0 -0
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/match_overrides.py +0 -0
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/rerun_finished.py +0 -0
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/skip_finished.py +0 -0
- {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/param/params.py +0 -0
- {hydraflow-0.8.0/tests/run → hydraflow-0.9.0/tests/entities}/filter.py +0 -0
- {hydraflow-0.8.0/tests/run → hydraflow-0.9.0/tests/entities}/values.py +0 -0
@@ -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(config_name="config", version_base=None)
|
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
|
```
|
@@ -48,31 +48,29 @@ pip install hydraflow
|
|
48
48
|
Here is a simple example to get you started with Hydraflow:
|
49
49
|
|
50
50
|
```python
|
51
|
-
import
|
52
|
-
|
53
|
-
import mlflow
|
51
|
+
from __future__ import annotations
|
52
|
+
|
54
53
|
from dataclasses import dataclass
|
55
|
-
from hydra.core.config_store import ConfigStore
|
56
54
|
from pathlib import Path
|
55
|
+
from typing import TYPE_CHECKING
|
57
56
|
|
58
|
-
|
59
|
-
class MySQLConfig:
|
60
|
-
host: str = "localhost"
|
61
|
-
port: int = 3306
|
57
|
+
import hydraflow
|
62
58
|
|
63
|
-
|
64
|
-
|
59
|
+
if TYPE_CHECKING:
|
60
|
+
from mlflow.entities import Run
|
61
|
+
|
62
|
+
|
63
|
+
@dataclass
|
64
|
+
class Config:
|
65
|
+
count: int = 1
|
66
|
+
name: str = "a"
|
65
67
|
|
66
|
-
@hydra.main(config_name="config", version_base=None)
|
67
|
-
def my_app(cfg: MySQLConfig) -> None:
|
68
|
-
# Set experiment by Hydra job name.
|
69
|
-
hydraflow.set_experiment()
|
70
68
|
|
71
|
-
|
72
|
-
|
73
|
-
|
69
|
+
@hydraflow.main(Config)
|
70
|
+
def app(run: Run, cfg: Config):
|
71
|
+
"""Your app code here."""
|
74
72
|
|
75
73
|
|
76
74
|
if __name__ == "__main__":
|
77
|
-
|
75
|
+
app()
|
78
76
|
```
|
@@ -12,16 +12,6 @@ There are two main steps to using Hydraflow:
|
|
12
12
|
--8<-- "apps/quickstart.py"
|
13
13
|
```
|
14
14
|
|
15
|
-
### Set the MLflow experiment
|
16
|
-
|
17
|
-
[`hydraflow.set_experiment`][] sets the MLflow experiment using the Hydra job name.
|
18
|
-
Optionally, it can also set the tracking URI with `uri` argument.
|
19
|
-
For example,
|
20
|
-
|
21
|
-
```python
|
22
|
-
hydraflow.set_experiment(uri="sqlite:///mlruns.db")
|
23
|
-
```
|
24
|
-
|
25
15
|
### Start a new MLflow run
|
26
16
|
|
27
17
|
[`hydraflow.start_run`][] starts a new MLflow run that logs the Hydra configuration.
|
@@ -64,10 +54,8 @@ $ python apps/quickstart.py -m width=400,600 height=100,200,300
|
|
64
54
|
### Run collection
|
65
55
|
|
66
56
|
```pycon exec="1" source="console" session="quickstart"
|
67
|
-
>>> import mlflow
|
68
|
-
>>> mlflow.set_experiment("quickstart")
|
69
57
|
>>> import hydraflow
|
70
|
-
>>> rc = hydraflow.list_runs()
|
58
|
+
>>> rc = hydraflow.list_runs("quickstart")
|
71
59
|
>>> print(rc)
|
72
60
|
```
|
73
61
|
|
@@ -107,16 +95,6 @@ $ python apps/quickstart.py -m width=400,600 height=100,200,300
|
|
107
95
|
>>> print(filtered)
|
108
96
|
```
|
109
97
|
|
110
|
-
```pycon exec="1" source="console" session="quickstart"
|
111
|
-
>>> run = rc.find(height=100)
|
112
|
-
>>> print(run.data.params)
|
113
|
-
```
|
114
|
-
|
115
|
-
```pycon exec="1" source="console" session="quickstart"
|
116
|
-
>>> run = rc.find_last(height=100)
|
117
|
-
>>> print(run.data.params)
|
118
|
-
```
|
119
|
-
|
120
98
|
### Group runs
|
121
99
|
|
122
100
|
```pycon exec="1" source="console" session="quickstart"
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "hydraflow"
|
7
|
-
version = "0.
|
7
|
+
version = "0.9.0"
|
8
8
|
description = "Hydraflow integrates Hydra and MLflow to manage and track machine learning experiments."
|
9
9
|
readme = "README.md"
|
10
10
|
license = { file = "LICENSE" }
|
@@ -19,7 +19,14 @@ classifiers = [
|
|
19
19
|
"Programming Language :: Python :: 3.13",
|
20
20
|
]
|
21
21
|
requires-python = ">=3.10"
|
22
|
-
dependencies = [
|
22
|
+
dependencies = [
|
23
|
+
"hydra-core>=1.3",
|
24
|
+
"mlflow>=2.15",
|
25
|
+
"omegaconf",
|
26
|
+
"rich",
|
27
|
+
"typer",
|
28
|
+
"ulid",
|
29
|
+
]
|
23
30
|
|
24
31
|
[project.urls]
|
25
32
|
Documentation = "https://daizutabi.github.io/hydraflow/"
|
@@ -44,6 +51,7 @@ addopts = [
|
|
44
51
|
"--cov=hydraflow",
|
45
52
|
"--cov-report=lcov:lcov.info",
|
46
53
|
"--dist=loadgroup",
|
54
|
+
"--doctest-modules",
|
47
55
|
"-n8",
|
48
56
|
]
|
49
57
|
filterwarnings = [
|
@@ -67,6 +75,7 @@ ignore = [
|
|
67
75
|
"ANN003",
|
68
76
|
"ANN401",
|
69
77
|
"B904",
|
78
|
+
"D104",
|
70
79
|
"D105",
|
71
80
|
"D107",
|
72
81
|
"D203",
|
@@ -79,13 +88,15 @@ ignore = [
|
|
79
88
|
"PLR0913",
|
80
89
|
"PLR1704",
|
81
90
|
"PLR2004",
|
91
|
+
"S603",
|
82
92
|
"SIM102",
|
83
93
|
"SIM108",
|
84
94
|
"TRY003",
|
85
95
|
]
|
86
96
|
|
87
97
|
[tool.ruff.lint.per-file-ignores]
|
88
|
-
"tests/*" = ["A001", "ANN", "ARG", "D", "FBT", "PD", "PLR", "PT", "S", "SLF"]
|
89
98
|
"apps/*.py" = ["D", "G", "INP"]
|
90
|
-
"src/hydraflow/main.py" = ["ANN201", "D401"]
|
91
99
|
"src/hydraflow/cli.py" = ["ANN", "D"]
|
100
|
+
"src/hydraflow/core/main.py" = ["ANN201", "D401"]
|
101
|
+
"src/hydraflow/executor/conf.py" = ["ANN", "D"]
|
102
|
+
"tests/*" = ["A001", "ANN", "ARG", "D", "FBT", "PD", "PLR", "PT", "S", "SLF"]
|
@@ -1,16 +1,16 @@
|
|
1
1
|
"""Integrate Hydra and MLflow to manage and track machine learning experiments."""
|
2
2
|
|
3
|
-
from hydraflow.context import chdir_artifact, log_run, start_run
|
4
|
-
from hydraflow.
|
5
|
-
from hydraflow.mlflow import list_run_ids, list_run_paths, list_runs
|
6
|
-
from hydraflow.run_collection import RunCollection
|
7
|
-
from hydraflow.utils import (
|
3
|
+
from hydraflow.core.context import chdir_artifact, log_run, start_run
|
4
|
+
from hydraflow.core.io import (
|
8
5
|
get_artifact_dir,
|
9
6
|
get_artifact_path,
|
10
7
|
get_hydra_output_dir,
|
11
8
|
load_config,
|
12
9
|
remove_run,
|
13
10
|
)
|
11
|
+
from hydraflow.core.main import main
|
12
|
+
from hydraflow.core.mlflow import list_run_ids, list_run_paths, list_runs
|
13
|
+
from hydraflow.entities.run_collection import RunCollection
|
14
14
|
|
15
15
|
__all__ = [
|
16
16
|
"RunCollection",
|
@@ -0,0 +1,67 @@
|
|
1
|
+
"""Hydraflow CLI."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import TYPE_CHECKING, Annotated
|
6
|
+
|
7
|
+
import typer
|
8
|
+
from rich.console import Console
|
9
|
+
from typer import Argument, Option
|
10
|
+
|
11
|
+
from hydraflow.executor.io import load_config
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from hydraflow.executor.job import Job
|
15
|
+
|
16
|
+
app = typer.Typer(add_completion=False)
|
17
|
+
console = Console()
|
18
|
+
|
19
|
+
|
20
|
+
def get_job(name: str) -> Job:
|
21
|
+
cfg = load_config()
|
22
|
+
job = cfg.jobs[name]
|
23
|
+
|
24
|
+
if not job.name:
|
25
|
+
job.name = name
|
26
|
+
|
27
|
+
return job
|
28
|
+
|
29
|
+
|
30
|
+
@app.command()
|
31
|
+
def run(
|
32
|
+
name: Annotated[str, Argument(help="Job name.", show_default=False)],
|
33
|
+
) -> None:
|
34
|
+
"""Run a job."""
|
35
|
+
import mlflow
|
36
|
+
|
37
|
+
from hydraflow.executor.job import multirun
|
38
|
+
|
39
|
+
job = get_job(name)
|
40
|
+
mlflow.set_experiment(job.name)
|
41
|
+
multirun(job)
|
42
|
+
|
43
|
+
|
44
|
+
@app.command()
|
45
|
+
def show(
|
46
|
+
name: Annotated[str, Argument(help="Job name.", show_default=False)],
|
47
|
+
) -> None:
|
48
|
+
"""Show a job."""
|
49
|
+
from hydraflow.executor.job import show
|
50
|
+
|
51
|
+
job = get_job(name)
|
52
|
+
show(job)
|
53
|
+
|
54
|
+
|
55
|
+
@app.callback(invoke_without_command=True)
|
56
|
+
def callback(
|
57
|
+
*,
|
58
|
+
version: Annotated[
|
59
|
+
bool,
|
60
|
+
Option("--version", help="Show the version and exit."),
|
61
|
+
] = False,
|
62
|
+
) -> None:
|
63
|
+
if version:
|
64
|
+
import importlib.metadata
|
65
|
+
|
66
|
+
typer.echo(f"hydraflow {importlib.metadata.version('hydraflow')}")
|
67
|
+
raise typer.Exit
|
@@ -12,8 +12,9 @@ import mlflow
|
|
12
12
|
import mlflow.artifacts
|
13
13
|
from hydra.core.hydra_config import HydraConfig
|
14
14
|
|
15
|
-
from hydraflow.
|
16
|
-
|
15
|
+
from hydraflow.core.io import get_artifact_dir
|
16
|
+
|
17
|
+
from .mlflow import log_params, log_text
|
17
18
|
|
18
19
|
if TYPE_CHECKING:
|
19
20
|
from collections.abc import Iterator
|
@@ -7,6 +7,7 @@ management.
|
|
7
7
|
|
8
8
|
The main functionality is provided through the `main` decorator, which can be
|
9
9
|
used to wrap experiment entry points. This decorator handles:
|
10
|
+
|
10
11
|
- Configuration management via Hydra
|
11
12
|
- Experiment tracking via MLflow
|
12
13
|
- Run deduplication based on configurations
|
@@ -44,11 +45,12 @@ from mlflow.entities import RunStatus
|
|
44
45
|
from omegaconf import OmegaConf
|
45
46
|
|
46
47
|
import hydraflow
|
47
|
-
from hydraflow.
|
48
|
+
from hydraflow.core.io import file_uri_to_path
|
48
49
|
|
49
50
|
if TYPE_CHECKING:
|
50
51
|
from collections.abc import Callable
|
51
52
|
from pathlib import Path
|
53
|
+
from typing import Any
|
52
54
|
|
53
55
|
from mlflow.entities import Run
|
54
56
|
|
@@ -115,7 +117,7 @@ def main(
|
|
115
117
|
return decorator
|
116
118
|
|
117
119
|
|
118
|
-
def get_run_id(uri: str, config:
|
120
|
+
def get_run_id(uri: str, config: Any, overrides: list[str] | None) -> str | None:
|
119
121
|
"""Try to get the run ID for the given configuration.
|
120
122
|
|
121
123
|
If the run is not found, the function will return None.
|
@@ -137,7 +139,7 @@ def get_run_id(uri: str, config: object, overrides: list[str] | None) -> str | N
|
|
137
139
|
return None
|
138
140
|
|
139
141
|
|
140
|
-
def equals(run_dir: Path, config:
|
142
|
+
def equals(run_dir: Path, config: Any, overrides: list[str] | None) -> bool:
|
141
143
|
"""Check if the run directory matches the given configuration or overrides.
|
142
144
|
|
143
145
|
Args:
|
@@ -13,9 +13,10 @@ import joblib
|
|
13
13
|
import mlflow
|
14
14
|
import mlflow.artifacts
|
15
15
|
|
16
|
-
from hydraflow.
|
17
|
-
from hydraflow.run_collection import RunCollection
|
18
|
-
|
16
|
+
from hydraflow.core.io import file_uri_to_path, get_artifact_dir
|
17
|
+
from hydraflow.entities.run_collection import RunCollection
|
18
|
+
|
19
|
+
from .config import iter_params
|
19
20
|
|
20
21
|
if TYPE_CHECKING:
|
21
22
|
from pathlib import Path
|
@@ -25,12 +25,13 @@ from typing import TYPE_CHECKING, Any, overload
|
|
25
25
|
|
26
26
|
from mlflow.entities import RunStatus
|
27
27
|
|
28
|
-
import hydraflow.param
|
29
|
-
from hydraflow.config import iter_params, select_config, select_overrides
|
30
|
-
from hydraflow.
|
31
|
-
from hydraflow.
|
32
|
-
|
33
|
-
from
|
28
|
+
import hydraflow.core.param
|
29
|
+
from hydraflow.core.config import iter_params, select_config, select_overrides
|
30
|
+
from hydraflow.core.io import load_config
|
31
|
+
from hydraflow.core.param import get_params, get_values
|
32
|
+
|
33
|
+
from .run_data import RunCollectionData
|
34
|
+
from .run_info import RunCollectionInfo
|
34
35
|
|
35
36
|
if TYPE_CHECKING:
|
36
37
|
from collections.abc import Callable, Iterator
|
@@ -478,7 +479,7 @@ def _param_matches(run: Run, key: str, value: Any) -> bool:
|
|
478
479
|
if param == "None":
|
479
480
|
return value is None or value == "None"
|
480
481
|
|
481
|
-
return hydraflow.param.match(param, value)
|
482
|
+
return hydraflow.core.param.match(param, value)
|
482
483
|
|
483
484
|
|
484
485
|
def filter_runs(
|
@@ -6,14 +6,14 @@ from typing import TYPE_CHECKING
|
|
6
6
|
|
7
7
|
from pandas import DataFrame
|
8
8
|
|
9
|
-
from hydraflow.config import iter_params
|
10
|
-
from hydraflow.
|
9
|
+
from hydraflow.core.config import iter_params
|
10
|
+
from hydraflow.core.io import load_config
|
11
11
|
|
12
12
|
if TYPE_CHECKING:
|
13
13
|
from collections.abc import Iterable
|
14
14
|
from typing import Any
|
15
15
|
|
16
|
-
from
|
16
|
+
from .run_collection import RunCollection
|
17
17
|
|
18
18
|
|
19
19
|
class RunCollectionData:
|
@@ -4,12 +4,12 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
from typing import TYPE_CHECKING
|
6
6
|
|
7
|
-
from hydraflow.
|
7
|
+
from hydraflow.core.io import get_artifact_dir
|
8
8
|
|
9
9
|
if TYPE_CHECKING:
|
10
10
|
from pathlib import Path
|
11
11
|
|
12
|
-
from
|
12
|
+
from .run_collection import RunCollection
|
13
13
|
|
14
14
|
|
15
15
|
class RunCollectionInfo:
|
@@ -0,0 +1,23 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass, field
|
4
|
+
|
5
|
+
|
6
|
+
@dataclass
|
7
|
+
class Step:
|
8
|
+
args: str = ""
|
9
|
+
batch: str = ""
|
10
|
+
options: str = ""
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass
|
14
|
+
class Job:
|
15
|
+
name: str = ""
|
16
|
+
run: str = ""
|
17
|
+
call: str = ""
|
18
|
+
steps: list[Step] = field(default_factory=list)
|
19
|
+
|
20
|
+
|
21
|
+
@dataclass
|
22
|
+
class HydraflowConf:
|
23
|
+
jobs: dict[str, Job] = field(default_factory=dict)
|
@@ -0,0 +1,34 @@
|
|
1
|
+
"""Hydraflow jobs IO."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from pathlib import Path
|
6
|
+
|
7
|
+
from omegaconf import OmegaConf
|
8
|
+
|
9
|
+
from .conf import HydraflowConf
|
10
|
+
|
11
|
+
|
12
|
+
def find_config_file() -> Path | None:
|
13
|
+
"""Find the hydraflow config file."""
|
14
|
+
if Path("hydraflow.yaml").exists():
|
15
|
+
return Path("hydraflow.yaml")
|
16
|
+
|
17
|
+
if Path("hydraflow.yml").exists():
|
18
|
+
return Path("hydraflow.yml")
|
19
|
+
|
20
|
+
return None
|
21
|
+
|
22
|
+
|
23
|
+
def load_config() -> HydraflowConf:
|
24
|
+
"""Load the hydraflow config."""
|
25
|
+
schema = OmegaConf.structured(HydraflowConf)
|
26
|
+
|
27
|
+
path = find_config_file()
|
28
|
+
|
29
|
+
if path is None:
|
30
|
+
return schema
|
31
|
+
|
32
|
+
cfg = OmegaConf.load(path)
|
33
|
+
|
34
|
+
return OmegaConf.merge(schema, cfg) # type: ignore
|
@@ -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
|