hydraflow 0.8.0__tar.gz → 0.9.1__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 (98) hide show
  1. {hydraflow-0.8.0 → hydraflow-0.9.1}/PKG-INFO +18 -19
  2. {hydraflow-0.8.0 → hydraflow-0.9.1}/README.md +16 -18
  3. {hydraflow-0.8.0 → hydraflow-0.9.1}/docs/usage/quickstart.md +1 -23
  4. {hydraflow-0.8.0 → hydraflow-0.9.1}/pyproject.toml +15 -4
  5. hydraflow-0.9.1/src/hydraflow/__init__.py +37 -0
  6. hydraflow-0.9.1/src/hydraflow/cli.py +67 -0
  7. {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.1/src/hydraflow/core}/context.py +3 -2
  8. hydraflow-0.8.0/src/hydraflow/utils.py → hydraflow-0.9.1/src/hydraflow/core/io.py +72 -1
  9. {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.1/src/hydraflow/core}/main.py +5 -3
  10. {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.1/src/hydraflow/core}/mlflow.py +4 -3
  11. {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.1/src/hydraflow/entities}/run_collection.py +8 -7
  12. {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.1/src/hydraflow/entities}/run_data.py +3 -3
  13. {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.1/src/hydraflow/entities}/run_info.py +2 -2
  14. hydraflow-0.9.1/src/hydraflow/executor/conf.py +23 -0
  15. hydraflow-0.9.1/src/hydraflow/executor/io.py +34 -0
  16. hydraflow-0.9.1/src/hydraflow/executor/job.py +152 -0
  17. hydraflow-0.9.1/src/hydraflow/executor/parser.py +397 -0
  18. hydraflow-0.9.1/tests/cli/app.py +24 -0
  19. hydraflow-0.9.1/tests/cli/conftest.py +12 -0
  20. hydraflow-0.9.1/tests/cli/hydraflow.yaml +13 -0
  21. hydraflow-0.9.1/tests/cli/test_run.py +33 -0
  22. hydraflow-0.9.1/tests/cli/test_setup.py +7 -0
  23. hydraflow-0.9.1/tests/cli/test_show.py +23 -0
  24. {hydraflow-0.8.0 → hydraflow-0.9.1}/tests/conftest.py +10 -5
  25. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/config/test_config.py +2 -2
  26. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/config/test_params.py +1 -1
  27. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/context/test_chdir.py +6 -3
  28. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/context/test_log_run.py +6 -3
  29. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/context/test_start_run.py +6 -3
  30. hydraflow-0.9.1/tests/core/io/__init__.py +0 -0
  31. hydraflow-0.8.0/tests/utils/test_utils.py → hydraflow-0.9.1/tests/core/io/test_hydra_dir.py +10 -12
  32. hydraflow-0.9.1/tests/core/io/test_iter_dirs.py +82 -0
  33. {hydraflow-0.8.0/tests/utils → hydraflow-0.9.1/tests/core/io}/test_run.py +4 -4
  34. hydraflow-0.9.1/tests/core/main/__init__.py +0 -0
  35. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/main/test_default.py +7 -6
  36. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/main/test_force_new_run.py +6 -3
  37. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/main/test_match_overrides.py +7 -4
  38. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/main/test_rerun_finished.py +6 -3
  39. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/main/test_skip_finished.py +8 -6
  40. hydraflow-0.9.1/tests/core/param/__init__.py +0 -0
  41. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/param/test_param.py +6 -6
  42. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/param/test_params.py +10 -7
  43. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/test_mlflow.py +12 -5
  44. hydraflow-0.9.1/tests/entities/__init__.py +0 -0
  45. {hydraflow-0.8.0/tests/run → hydraflow-0.9.1/tests/entities}/test_collection.py +5 -5
  46. {hydraflow-0.8.0/tests/run → hydraflow-0.9.1/tests/entities}/test_data.py +2 -2
  47. {hydraflow-0.8.0/tests/run → hydraflow-0.9.1/tests/entities}/test_filter.py +6 -3
  48. {hydraflow-0.8.0/tests/run → hydraflow-0.9.1/tests/entities}/test_info.py +2 -2
  49. {hydraflow-0.8.0/tests/run → hydraflow-0.9.1/tests/entities}/test_values.py +5 -2
  50. hydraflow-0.9.1/tests/executor/__init__.py +0 -0
  51. hydraflow-0.9.1/tests/executor/conftest.py +30 -0
  52. hydraflow-0.9.1/tests/executor/echo.py +17 -0
  53. hydraflow-0.9.1/tests/executor/test_args.py +19 -0
  54. hydraflow-0.9.1/tests/executor/test_conf.py +34 -0
  55. hydraflow-0.9.1/tests/executor/test_io.py +18 -0
  56. hydraflow-0.9.1/tests/executor/test_job.py +127 -0
  57. hydraflow-0.9.1/tests/executor/test_parser.py +220 -0
  58. hydraflow-0.8.0/hydraflow.yaml +0 -5
  59. hydraflow-0.8.0/src/hydraflow/__init__.py +0 -29
  60. hydraflow-0.8.0/src/hydraflow/cli.py +0 -75
  61. hydraflow-0.8.0/tests/cli/conftest.py +0 -9
  62. hydraflow-0.8.0/tests/cli/test_run.py +0 -18
  63. hydraflow-0.8.0/tests/cli/test_show.py +0 -52
  64. {hydraflow-0.8.0 → hydraflow-0.9.1}/.devcontainer/devcontainer.json +0 -0
  65. {hydraflow-0.8.0 → hydraflow-0.9.1}/.devcontainer/postCreate.sh +0 -0
  66. {hydraflow-0.8.0 → hydraflow-0.9.1}/.devcontainer/starship.toml +0 -0
  67. {hydraflow-0.8.0 → hydraflow-0.9.1}/.gitattributes +0 -0
  68. {hydraflow-0.8.0 → hydraflow-0.9.1}/.github/workflows/ci.yaml +0 -0
  69. {hydraflow-0.8.0 → hydraflow-0.9.1}/.github/workflows/docs.yaml +0 -0
  70. {hydraflow-0.8.0 → hydraflow-0.9.1}/.gitignore +0 -0
  71. {hydraflow-0.8.0 → hydraflow-0.9.1}/LICENSE +0 -0
  72. {hydraflow-0.8.0 → hydraflow-0.9.1}/apps/quickstart.py +0 -0
  73. {hydraflow-0.8.0 → hydraflow-0.9.1}/docs/index.md +0 -0
  74. {hydraflow-0.8.0 → hydraflow-0.9.1}/mkdocs.yaml +0 -0
  75. {hydraflow-0.8.0/tests → hydraflow-0.9.1/src/hydraflow/core}/__init__.py +0 -0
  76. {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.1/src/hydraflow/core}/config.py +0 -0
  77. {hydraflow-0.8.0/src/hydraflow → hydraflow-0.9.1/src/hydraflow/core}/param.py +0 -0
  78. {hydraflow-0.8.0/tests/cli → hydraflow-0.9.1/src/hydraflow/entities}/__init__.py +0 -0
  79. {hydraflow-0.8.0/tests/config → hydraflow-0.9.1/src/hydraflow/executor}/__init__.py +0 -0
  80. {hydraflow-0.8.0 → hydraflow-0.9.1}/src/hydraflow/py.typed +0 -0
  81. {hydraflow-0.8.0/tests/context → hydraflow-0.9.1/tests}/__init__.py +0 -0
  82. {hydraflow-0.8.0/tests/main → hydraflow-0.9.1/tests/cli}/__init__.py +0 -0
  83. {hydraflow-0.8.0 → hydraflow-0.9.1}/tests/cli/test_version.py +0 -0
  84. {hydraflow-0.8.0/tests/param → hydraflow-0.9.1/tests/core}/__init__.py +0 -0
  85. {hydraflow-0.8.0/tests/run → hydraflow-0.9.1/tests/core/config}/__init__.py +0 -0
  86. {hydraflow-0.8.0/tests/utils → hydraflow-0.9.1/tests/core/context}/__init__.py +0 -0
  87. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/context/chdir.py +0 -0
  88. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/context/log_run.py +0 -0
  89. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/context/start_run.py +0 -0
  90. /hydraflow-0.8.0/tests/utils/utils.py → /hydraflow-0.9.1/tests/core/io/hydra_dir.py +0 -0
  91. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/main/default.py +0 -0
  92. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/main/force_new_run.py +0 -0
  93. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/main/match_overrides.py +0 -0
  94. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/main/rerun_finished.py +0 -0
  95. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/main/skip_finished.py +0 -0
  96. {hydraflow-0.8.0/tests → hydraflow-0.9.1/tests/core}/param/params.py +0 -0
  97. {hydraflow-0.8.0/tests/run → hydraflow-0.9.1/tests/entities}/filter.py +0 -0
  98. {hydraflow-0.8.0/tests/run → hydraflow-0.9.1/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.1
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.1"
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"]
@@ -0,0 +1,37 @@
1
+ """Integrate Hydra and MLflow to manage and track machine learning experiments."""
2
+
3
+ from hydraflow.core.context import chdir_artifact, log_run, start_run
4
+ from hydraflow.core.io import (
5
+ get_artifact_dir,
6
+ get_artifact_path,
7
+ get_hydra_output_dir,
8
+ iter_artifact_paths,
9
+ iter_artifacts_dirs,
10
+ iter_experiment_dirs,
11
+ iter_run_dirs,
12
+ load_config,
13
+ remove_run,
14
+ )
15
+ from hydraflow.core.main import main
16
+ from hydraflow.core.mlflow import list_run_ids, list_run_paths, list_runs
17
+ from hydraflow.entities.run_collection import RunCollection
18
+
19
+ __all__ = [
20
+ "RunCollection",
21
+ "chdir_artifact",
22
+ "get_artifact_dir",
23
+ "get_artifact_path",
24
+ "get_hydra_output_dir",
25
+ "iter_artifact_paths",
26
+ "iter_artifacts_dirs",
27
+ "iter_experiment_dirs",
28
+ "iter_run_dirs",
29
+ "list_run_ids",
30
+ "list_run_paths",
31
+ "list_runs",
32
+ "load_config",
33
+ "log_run",
34
+ "main",
35
+ "remove_run",
36
+ "start_run",
37
+ ]
@@ -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
@@ -15,7 +15,7 @@ from mlflow.entities import Run
15
15
  from omegaconf import DictConfig, ListConfig, OmegaConf
16
16
 
17
17
  if TYPE_CHECKING:
18
- from collections.abc import Iterable
18
+ from collections.abc import Iterable, Iterator
19
19
 
20
20
 
21
21
  def file_uri_to_path(uri: str) -> Path:
@@ -147,3 +147,74 @@ def remove_run(run: Run | Iterable[Run]) -> None:
147
147
  return
148
148
 
149
149
  shutil.rmtree(get_artifact_dir(run).parent)
150
+
151
+
152
+ def get_root_dir(uri: str | Path | None = None) -> Path:
153
+ """Get the root directory for the MLflow tracking server."""
154
+ if uri is not None:
155
+ return Path(uri).absolute()
156
+
157
+ uri = mlflow.get_tracking_uri()
158
+
159
+ if uri.startswith("file:"):
160
+ return file_uri_to_path(uri)
161
+
162
+ return Path(uri).absolute()
163
+
164
+
165
+ def get_experiment_name(path: Path) -> str | None:
166
+ """Get the experiment name from the meta file."""
167
+ metafile = path / "meta.yaml"
168
+ if not metafile.exists():
169
+ return None
170
+ lines = metafile.read_text().splitlines()
171
+ for line in lines:
172
+ if line.startswith("name:"):
173
+ return line.split(":")[1].strip()
174
+ return None
175
+
176
+
177
+ def iter_experiment_dirs(
178
+ experiment_names: str | list[str] | None = None,
179
+ root_dir: str | Path | None = None,
180
+ ) -> Iterator[Path]:
181
+ """Iterate over the experiment directories in the root directory."""
182
+ if isinstance(experiment_names, str):
183
+ experiment_names = [experiment_names]
184
+
185
+ root_dir = get_root_dir(root_dir)
186
+ for path in root_dir.iterdir():
187
+ if path.is_dir() and path.name not in [".trash", "0"]:
188
+ if name := get_experiment_name(path):
189
+ if experiment_names is None or name in experiment_names:
190
+ yield path
191
+
192
+
193
+ def iter_run_dirs(
194
+ experiment_names: str | list[str] | None = None,
195
+ root_dir: str | Path | None = None,
196
+ ) -> Iterator[Path]:
197
+ """Iterate over the run directories in the root directory."""
198
+ for experiment_dir in iter_experiment_dirs(experiment_names, root_dir):
199
+ for path in experiment_dir.iterdir():
200
+ if path.is_dir() and (path / "artifacts").exists():
201
+ yield path
202
+
203
+
204
+ def iter_artifacts_dirs(
205
+ experiment_names: str | list[str] | None = None,
206
+ root_dir: str | Path | None = None,
207
+ ) -> Iterator[Path]:
208
+ """Iterate over the artifacts directories in the root directory."""
209
+ for path in iter_run_dirs(experiment_names, root_dir):
210
+ yield path / "artifacts"
211
+
212
+
213
+ def iter_artifact_paths(
214
+ artifact_path: str | Path,
215
+ experiment_names: str | list[str] | None = None,
216
+ root_dir: str | Path | None = None,
217
+ ) -> Iterator[Path]:
218
+ """Iterate over the artifact paths in the root directory."""
219
+ for path in iter_artifacts_dirs(experiment_names, root_dir):
220
+ yield path / artifact_path
@@ -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