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.
Files changed (96) hide show
  1. {hydraflow-0.8.0 → hydraflow-0.9.0}/PKG-INFO +18 -19
  2. {hydraflow-0.8.0 → hydraflow-0.9.0}/README.md +16 -18
  3. {hydraflow-0.8.0 → hydraflow-0.9.0}/docs/usage/quickstart.md +1 -23
  4. {hydraflow-0.8.0 → hydraflow-0.9.0}/pyproject.toml +15 -4
  5. {hydraflow-0.8.0 → hydraflow-0.9.0}/src/hydraflow/__init__.py +5 -5
  6. hydraflow-0.9.0/src/hydraflow/cli.py +67 -0
  7. {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.0/src/hydraflow/core}/context.py +3 -2
  8. {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.0/src/hydraflow/core}/main.py +5 -3
  9. {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.0/src/hydraflow/core}/mlflow.py +4 -3
  10. {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.0/src/hydraflow/entities}/run_collection.py +8 -7
  11. {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.0/src/hydraflow/entities}/run_data.py +3 -3
  12. {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.0/src/hydraflow/entities}/run_info.py +2 -2
  13. hydraflow-0.9.0/src/hydraflow/executor/conf.py +23 -0
  14. hydraflow-0.9.0/src/hydraflow/executor/io.py +34 -0
  15. hydraflow-0.9.0/src/hydraflow/executor/job.py +152 -0
  16. hydraflow-0.9.0/src/hydraflow/executor/parser.py +397 -0
  17. hydraflow-0.9.0/tests/cli/app.py +24 -0
  18. hydraflow-0.9.0/tests/cli/conftest.py +12 -0
  19. hydraflow-0.9.0/tests/cli/hydraflow.yaml +13 -0
  20. hydraflow-0.9.0/tests/cli/test_run.py +33 -0
  21. hydraflow-0.9.0/tests/cli/test_setup.py +7 -0
  22. hydraflow-0.9.0/tests/cli/test_show.py +23 -0
  23. {hydraflow-0.8.0 → hydraflow-0.9.0}/tests/conftest.py +10 -5
  24. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/config/test_config.py +2 -2
  25. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/config/test_params.py +1 -1
  26. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/context/test_chdir.py +6 -3
  27. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/context/test_log_run.py +6 -3
  28. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/context/test_start_run.py +6 -3
  29. hydraflow-0.9.0/tests/core/io/__init__.py +0 -0
  30. hydraflow-0.8.0/tests/utils/test_utils.py → hydraflow-0.9.0/tests/core/io/test_hydra_dir.py +10 -12
  31. {hydraflow-0.8.0/tests/utils → hydraflow-0.9.0/tests/core/io}/test_run.py +4 -4
  32. hydraflow-0.9.0/tests/core/main/__init__.py +0 -0
  33. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/test_default.py +7 -6
  34. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/test_force_new_run.py +6 -3
  35. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/test_match_overrides.py +7 -4
  36. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/test_rerun_finished.py +6 -3
  37. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/test_skip_finished.py +8 -6
  38. hydraflow-0.9.0/tests/core/param/__init__.py +0 -0
  39. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/param/test_param.py +6 -6
  40. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/param/test_params.py +10 -7
  41. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/test_mlflow.py +5 -5
  42. hydraflow-0.9.0/tests/entities/__init__.py +0 -0
  43. {hydraflow-0.8.0/tests/run → hydraflow-0.9.0/tests/entities}/test_collection.py +5 -5
  44. {hydraflow-0.8.0/tests/run → hydraflow-0.9.0/tests/entities}/test_data.py +2 -2
  45. {hydraflow-0.8.0/tests/run → hydraflow-0.9.0/tests/entities}/test_filter.py +6 -3
  46. {hydraflow-0.8.0/tests/run → hydraflow-0.9.0/tests/entities}/test_info.py +2 -2
  47. {hydraflow-0.8.0/tests/run → hydraflow-0.9.0/tests/entities}/test_values.py +5 -2
  48. hydraflow-0.9.0/tests/executor/__init__.py +0 -0
  49. hydraflow-0.9.0/tests/executor/conftest.py +30 -0
  50. hydraflow-0.9.0/tests/executor/echo.py +17 -0
  51. hydraflow-0.9.0/tests/executor/test_args.py +19 -0
  52. hydraflow-0.9.0/tests/executor/test_conf.py +34 -0
  53. hydraflow-0.9.0/tests/executor/test_io.py +18 -0
  54. hydraflow-0.9.0/tests/executor/test_job.py +127 -0
  55. hydraflow-0.9.0/tests/executor/test_parser.py +220 -0
  56. hydraflow-0.8.0/hydraflow.yaml +0 -5
  57. hydraflow-0.8.0/src/hydraflow/cli.py +0 -75
  58. hydraflow-0.8.0/tests/cli/conftest.py +0 -9
  59. hydraflow-0.8.0/tests/cli/test_run.py +0 -18
  60. hydraflow-0.8.0/tests/cli/test_show.py +0 -52
  61. {hydraflow-0.8.0 → hydraflow-0.9.0}/.devcontainer/devcontainer.json +0 -0
  62. {hydraflow-0.8.0 → hydraflow-0.9.0}/.devcontainer/postCreate.sh +0 -0
  63. {hydraflow-0.8.0 → hydraflow-0.9.0}/.devcontainer/starship.toml +0 -0
  64. {hydraflow-0.8.0 → hydraflow-0.9.0}/.gitattributes +0 -0
  65. {hydraflow-0.8.0 → hydraflow-0.9.0}/.github/workflows/ci.yaml +0 -0
  66. {hydraflow-0.8.0 → hydraflow-0.9.0}/.github/workflows/docs.yaml +0 -0
  67. {hydraflow-0.8.0 → hydraflow-0.9.0}/.gitignore +0 -0
  68. {hydraflow-0.8.0 → hydraflow-0.9.0}/LICENSE +0 -0
  69. {hydraflow-0.8.0 → hydraflow-0.9.0}/apps/quickstart.py +0 -0
  70. {hydraflow-0.8.0 → hydraflow-0.9.0}/docs/index.md +0 -0
  71. {hydraflow-0.8.0 → hydraflow-0.9.0}/mkdocs.yaml +0 -0
  72. {hydraflow-0.8.0/tests → hydraflow-0.9.0/src/hydraflow/core}/__init__.py +0 -0
  73. {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.0/src/hydraflow/core}/config.py +0 -0
  74. /hydraflow-0.8.0/src/hydraflow/utils.py → /hydraflow-0.9.0/src/hydraflow/core/io.py +0 -0
  75. {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.0/src/hydraflow/core}/param.py +0 -0
  76. {hydraflow-0.8.0/tests/cli → hydraflow-0.9.0/src/hydraflow/entities}/__init__.py +0 -0
  77. {hydraflow-0.8.0/tests/config → hydraflow-0.9.0/src/hydraflow/executor}/__init__.py +0 -0
  78. {hydraflow-0.8.0 → hydraflow-0.9.0}/src/hydraflow/py.typed +0 -0
  79. {hydraflow-0.8.0/tests/context → hydraflow-0.9.0/tests}/__init__.py +0 -0
  80. {hydraflow-0.8.0/tests/main → hydraflow-0.9.0/tests/cli}/__init__.py +0 -0
  81. {hydraflow-0.8.0 → hydraflow-0.9.0}/tests/cli/test_version.py +0 -0
  82. {hydraflow-0.8.0/tests/param → hydraflow-0.9.0/tests/core}/__init__.py +0 -0
  83. {hydraflow-0.8.0/tests/run → hydraflow-0.9.0/tests/core/config}/__init__.py +0 -0
  84. {hydraflow-0.8.0/tests/utils → hydraflow-0.9.0/tests/core/context}/__init__.py +0 -0
  85. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/context/chdir.py +0 -0
  86. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/context/log_run.py +0 -0
  87. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/context/start_run.py +0 -0
  88. /hydraflow-0.8.0/tests/utils/utils.py → /hydraflow-0.9.0/tests/core/io/hydra_dir.py +0 -0
  89. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/default.py +0 -0
  90. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/force_new_run.py +0 -0
  91. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/match_overrides.py +0 -0
  92. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/rerun_finished.py +0 -0
  93. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/main/skip_finished.py +0 -0
  94. {hydraflow-0.8.0/tests → hydraflow-0.9.0/tests/core}/param/params.py +0 -0
  95. {hydraflow-0.8.0/tests/run → hydraflow-0.9.0/tests/entities}/filter.py +0 -0
  96. {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.8.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 hydra
97
- import hydraflow
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
- @dataclass
104
- class MySQLConfig:
105
- host: str = "localhost"
106
- port: int = 3306
103
+ import hydraflow
107
104
 
108
- cs = ConfigStore.instance()
109
- cs.store(name="config", node=MySQLConfig)
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
- # Automatically log Hydra config as params.
117
- with hydraflow.start_run(cfg):
118
- # Your app code below.
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
- my_app()
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 hydra
52
- import hydraflow
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
- @dataclass
59
- class MySQLConfig:
60
- host: str = "localhost"
61
- port: int = 3306
57
+ import hydraflow
62
58
 
63
- cs = ConfigStore.instance()
64
- cs.store(name="config", node=MySQLConfig)
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
- # Automatically log Hydra config as params.
72
- with hydraflow.start_run(cfg):
73
- # Your app code below.
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
- my_app()
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.8.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 = ["hydra-core>=1.3", "mlflow>=2.15", "omegaconf", "rich", "typer"]
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.main import main
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.mlflow import log_params, log_text
16
- from hydraflow.utils import get_artifact_dir
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.utils import file_uri_to_path
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: object, overrides: list[str] | None) -> str | None:
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: object, overrides: list[str] | None) -> bool:
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.config import iter_params
17
- from hydraflow.run_collection import RunCollection
18
- from hydraflow.utils import file_uri_to_path, get_artifact_dir
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.param import get_params, get_values
31
- from hydraflow.run_data import RunCollectionData
32
- from hydraflow.run_info import RunCollectionInfo
33
- from hydraflow.utils import load_config
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.utils import load_config
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 hydraflow.run_collection import RunCollection
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.utils import get_artifact_dir
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 hydraflow.run_collection import RunCollection
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