hydraflow 0.3.0__tar.gz → 0.3.1__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. {hydraflow-0.3.0 → hydraflow-0.3.1}/.devcontainer/devcontainer.json +0 -1
  2. {hydraflow-0.3.0 → hydraflow-0.3.1}/.gitignore +4 -0
  3. {hydraflow-0.3.0 → hydraflow-0.3.1}/PKG-INFO +1 -1
  4. hydraflow-0.3.1/apps/hello.py +31 -0
  5. {hydraflow-0.3.0 → hydraflow-0.3.1}/mkdocs.yml +3 -0
  6. {hydraflow-0.3.0 → hydraflow-0.3.1}/pyproject.toml +6 -1
  7. {hydraflow-0.3.0 → hydraflow-0.3.1}/src/hydraflow/__init__.py +5 -9
  8. {hydraflow-0.3.0 → hydraflow-0.3.1}/src/hydraflow/config.py +10 -2
  9. {hydraflow-0.3.0 → hydraflow-0.3.1}/src/hydraflow/context.py +19 -0
  10. {hydraflow-0.3.0 → hydraflow-0.3.1}/src/hydraflow/mlflow.py +8 -2
  11. hydraflow-0.3.1/src/hydraflow/run_data.py +34 -0
  12. hydraflow-0.3.1/src/hydraflow/run_info.py +34 -0
  13. hydraflow-0.3.0/src/hydraflow/run_info.py → hydraflow-0.3.1/src/hydraflow/utils.py +23 -26
  14. {hydraflow-0.3.0 → hydraflow-0.3.1}/tests/scripts/app.py +5 -0
  15. {hydraflow-0.3.0 → hydraflow-0.3.1}/tests/test_app.py +17 -3
  16. hydraflow-0.3.0/src/hydraflow/run_data.py +0 -56
  17. {hydraflow-0.3.0 → hydraflow-0.3.1}/.devcontainer/postCreate.sh +0 -0
  18. {hydraflow-0.3.0 → hydraflow-0.3.1}/.devcontainer/starship.toml +0 -0
  19. {hydraflow-0.3.0 → hydraflow-0.3.1}/.gitattributes +0 -0
  20. {hydraflow-0.3.0 → hydraflow-0.3.1}/LICENSE +0 -0
  21. {hydraflow-0.3.0 → hydraflow-0.3.1}/README.md +0 -0
  22. {hydraflow-0.3.0 → hydraflow-0.3.1}/src/hydraflow/asyncio.py +0 -0
  23. {hydraflow-0.3.0 → hydraflow-0.3.1}/src/hydraflow/param.py +0 -0
  24. {hydraflow-0.3.0 → hydraflow-0.3.1}/src/hydraflow/progress.py +0 -0
  25. {hydraflow-0.3.0 → hydraflow-0.3.1}/src/hydraflow/py.typed +0 -0
  26. {hydraflow-0.3.0 → hydraflow-0.3.1}/src/hydraflow/run_collection.py +0 -0
  27. {hydraflow-0.3.0 → hydraflow-0.3.1}/tests/__init__.py +0 -0
  28. {hydraflow-0.3.0 → hydraflow-0.3.1}/tests/scripts/__init__.py +0 -0
  29. {hydraflow-0.3.0 → hydraflow-0.3.1}/tests/scripts/progress.py +0 -0
  30. {hydraflow-0.3.0 → hydraflow-0.3.1}/tests/scripts/watch.py +0 -0
  31. {hydraflow-0.3.0 → hydraflow-0.3.1}/tests/test_asyncio.py +0 -0
  32. {hydraflow-0.3.0 → hydraflow-0.3.1}/tests/test_config.py +0 -0
  33. {hydraflow-0.3.0 → hydraflow-0.3.1}/tests/test_context.py +0 -0
  34. {hydraflow-0.3.0 → hydraflow-0.3.1}/tests/test_log_run.py +0 -0
  35. {hydraflow-0.3.0 → hydraflow-0.3.1}/tests/test_mlflow.py +0 -0
  36. {hydraflow-0.3.0 → hydraflow-0.3.1}/tests/test_param.py +0 -0
  37. {hydraflow-0.3.0 → hydraflow-0.3.1}/tests/test_progress.py +0 -0
  38. {hydraflow-0.3.0 → hydraflow-0.3.1}/tests/test_run_collection.py +0 -0
  39. {hydraflow-0.3.0 → hydraflow-0.3.1}/tests/test_run_data.py +0 -0
  40. {hydraflow-0.3.0 → hydraflow-0.3.1}/tests/test_run_info.py +0 -0
  41. {hydraflow-0.3.0 → hydraflow-0.3.1}/tests/test_version.py +0 -0
  42. {hydraflow-0.3.0 → hydraflow-0.3.1}/tests/test_watch.py +0 -0
@@ -1,5 +1,4 @@
1
1
  {
2
- "name": "hydraflow",
3
2
  "image": "mcr.microsoft.com/vscode/devcontainers/python:3.12",
4
3
  "features": {
5
4
  "ghcr.io/devcontainers-contrib/features/starship:1": {},
@@ -1,7 +1,11 @@
1
+ *.db
1
2
  .coverage
2
3
  .env
3
4
  .venv/
4
5
  __pycache__/
5
6
  dist/
6
7
  lcov.info
8
+ mlruns/
9
+ multirun/
10
+ outputs/
7
11
  uv.lock
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: hydraflow
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Hydraflow integrates Hydra and MLflow to manage and track machine learning experiments.
5
5
  Project-URL: Documentation, https://github.com/daizutabi/hydraflow
6
6
  Project-URL: Source, https://github.com/daizutabi/hydraflow
@@ -0,0 +1,31 @@
1
+ import logging
2
+ from dataclasses import dataclass
3
+
4
+ import hydra
5
+ from hydra.core.config_store import ConfigStore
6
+
7
+ import hydraflow
8
+
9
+ log = logging.getLogger(__name__)
10
+
11
+
12
+ @dataclass
13
+ class Config:
14
+ width: int = 1024
15
+ height: int = 768
16
+
17
+
18
+ cs = ConfigStore.instance()
19
+ cs.store(name="config", node=Config)
20
+
21
+
22
+ @hydra.main(version_base=None, config_name="config")
23
+ def app(cfg: Config) -> None:
24
+ hydraflow.set_experiment()
25
+
26
+ with hydraflow.start_run(cfg):
27
+ log.info(f"{cfg.width=}, {cfg.height=}")
28
+
29
+
30
+ if __name__ == "__main__":
31
+ app()
@@ -38,6 +38,7 @@ theme:
38
38
  - navigation.tracking
39
39
  plugins:
40
40
  - search
41
+ - markdown-exec
41
42
  - mkapi
42
43
  markdown_extensions:
43
44
  - pymdownx.magiclink
@@ -50,4 +51,6 @@ markdown_extensions:
50
51
  alternate_style: true
51
52
  nav:
52
53
  - Home: index.md
54
+ - Usage:
55
+ - Hydra application: usage/hydra.md
53
56
  - Reference: $api/hydraflow.**
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hydraflow"
7
- version = "0.3.0"
7
+ version = "0.3.1"
8
8
  description = "Hydraflow integrates Hydra and MLflow to manage and track machine learning experiments."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -34,6 +34,10 @@ Issues = "https://github.com/daizutabi/hydraflow/issues"
34
34
 
35
35
  [tool.uv]
36
36
  dev-dependencies = [
37
+ "markdown-exec[ansi]",
38
+ "mkapi",
39
+ "mkdocs-material",
40
+ "mkdocs>=1.6",
37
41
  "pytest-asyncio",
38
42
  "pytest-clarity",
39
43
  "pytest-cov",
@@ -97,3 +101,4 @@ exclude = ["tests/scripts/*.py"]
97
101
  "SIM117",
98
102
  "SLF",
99
103
  ]
104
+ "apps/*.py" = ["INP", "D", "G", "T"]
@@ -1,19 +1,15 @@
1
- """Provide a collection of MLflow runs."""
1
+ """Integrate Hydra and MLflow to manage and track machine learning experiments."""
2
2
 
3
- from .context import chdir_artifact, log_run, start_run, watch
4
- from .mlflow import (
5
- list_runs,
6
- search_runs,
7
- set_experiment,
8
- )
3
+ from .context import chdir_artifact, chdir_hydra, log_run, start_run, watch
4
+ from .mlflow import list_runs, search_runs, set_experiment
9
5
  from .progress import multi_tasks_progress, parallel_progress
10
6
  from .run_collection import RunCollection
11
- from .run_data import load_config
12
- from .run_info import get_artifact_dir, get_hydra_output_dir
7
+ from .utils import get_artifact_dir, get_hydra_output_dir, load_config
13
8
 
14
9
  __all__ = [
15
10
  "RunCollection",
16
11
  "chdir_artifact",
12
+ "chdir_hydra",
17
13
  "get_artifact_dir",
18
14
  "get_hydra_output_dir",
19
15
  "list_runs",
@@ -54,7 +54,7 @@ def _iter_params(config: object, prefix: str = "") -> Iterator[tuple[str, Any]]:
54
54
  if isinstance(config, DictConfig):
55
55
  for key, value in config.items():
56
56
  if _is_param(value):
57
- yield f"{prefix}{key}", value
57
+ yield f"{prefix}{key}", _convert(value)
58
58
 
59
59
  else:
60
60
  yield from _iter_params(value, f"{prefix}{key}.")
@@ -62,7 +62,7 @@ def _iter_params(config: object, prefix: str = "") -> Iterator[tuple[str, Any]]:
62
62
  elif isinstance(config, ListConfig):
63
63
  for index, value in enumerate(config):
64
64
  if _is_param(value):
65
- yield f"{prefix}{index}", value
65
+ yield f"{prefix}{index}", _convert(value)
66
66
 
67
67
  else:
68
68
  yield from _iter_params(value, f"{prefix}{index}.")
@@ -78,3 +78,11 @@ def _is_param(value: object) -> bool:
78
78
  return False
79
79
 
80
80
  return True
81
+
82
+
83
+ def _convert(value: Any) -> Any:
84
+ """Convert the given value to a Python object."""
85
+ if isinstance(value, ListConfig):
86
+ return list(value)
87
+
88
+ return value
@@ -238,6 +238,25 @@ class Handler(PatternMatchingEventHandler):
238
238
  self.func(file)
239
239
 
240
240
 
241
+ @contextmanager
242
+ def chdir_hydra() -> Iterator[Path]:
243
+ """Change the current working directory to the hydra output directory.
244
+
245
+ This context manager changes the current working directory to the hydra output
246
+ directory. It ensures that the directory is changed back to the original
247
+ directory after the context is exited.
248
+ """
249
+ curdir = Path.cwd()
250
+ path = HydraConfig.get().runtime.output_dir
251
+
252
+ os.chdir(path)
253
+ try:
254
+ yield Path(path)
255
+
256
+ finally:
257
+ os.chdir(curdir)
258
+
259
+
241
260
  @contextmanager
242
261
  def chdir_artifact(
243
262
  run: Run,
@@ -207,8 +207,14 @@ def _list_runs(
207
207
  if experiment := mlflow.get_experiment_by_name(name):
208
208
  loc = experiment.artifact_location
209
209
 
210
- if isinstance(loc, str) and loc.startswith("file://"):
211
- path = Path(mlflow.artifacts.download_artifacts(loc))
210
+ if isinstance(loc, str):
211
+ if loc.startswith("file://"):
212
+ path = Path(mlflow.artifacts.download_artifacts(loc))
213
+ elif Path(loc).is_dir():
214
+ path = Path(loc)
215
+ else:
216
+ continue
217
+
212
218
  run_ids.extend(file.stem for file in path.iterdir() if file.is_dir())
213
219
 
214
220
  it = (joblib.delayed(mlflow.get_run)(run_id) for run_id in run_ids)
@@ -0,0 +1,34 @@
1
+ """Provide data about `RunCollection` instances."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from hydraflow.utils import load_config
8
+
9
+ if TYPE_CHECKING:
10
+ from omegaconf import DictConfig
11
+
12
+ from hydraflow.run_collection import RunCollection
13
+
14
+
15
+ class RunCollectionData:
16
+ """Provide data about a `RunCollection` instance."""
17
+
18
+ def __init__(self, runs: RunCollection) -> None:
19
+ self._runs = runs
20
+
21
+ @property
22
+ def params(self) -> list[dict[str, str]]:
23
+ """Get the parameters for each run in the collection."""
24
+ return [run.data.params for run in self._runs]
25
+
26
+ @property
27
+ def metrics(self) -> list[dict[str, float]]:
28
+ """Get the metrics for each run in the collection."""
29
+ return [run.data.metrics for run in self._runs]
30
+
31
+ @property
32
+ def config(self) -> list[DictConfig]:
33
+ """Get the configuration for each run in the collection."""
34
+ return [load_config(run) for run in self._runs]
@@ -0,0 +1,34 @@
1
+ """Provide information about `RunCollection` instances."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from hydraflow.utils import get_artifact_dir
8
+
9
+ if TYPE_CHECKING:
10
+ from pathlib import Path
11
+
12
+ from hydraflow.run_collection import RunCollection
13
+
14
+
15
+ class RunCollectionInfo:
16
+ """Provide information about a `RunCollection` instance."""
17
+
18
+ def __init__(self, runs: RunCollection) -> None:
19
+ self._runs = runs
20
+
21
+ @property
22
+ def run_id(self) -> list[str]:
23
+ """Get the run ID for each run in the collection."""
24
+ return [run.info.run_id for run in self._runs]
25
+
26
+ @property
27
+ def artifact_uri(self) -> list[str | None]:
28
+ """Get the artifact URI for each run in the collection."""
29
+ return [run.info.artifact_uri for run in self._runs]
30
+
31
+ @property
32
+ def artifact_dir(self) -> list[Path]:
33
+ """Get the artifact directory for each run in the collection."""
34
+ return [get_artifact_dir(run) for run in self._runs]
@@ -1,4 +1,4 @@
1
- """Provide information about MLflow runs."""
1
+ """Provide utility functions for HydraFlow."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -7,36 +7,13 @@ from typing import TYPE_CHECKING
7
7
 
8
8
  import mlflow
9
9
  from hydra.core.hydra_config import HydraConfig
10
+ from mlflow.entities import Run
10
11
  from mlflow.tracking import artifact_utils
11
- from omegaconf import OmegaConf
12
+ from omegaconf import DictConfig, OmegaConf
12
13
 
13
14
  if TYPE_CHECKING:
14
15
  from mlflow.entities import Run
15
16
 
16
- from hydraflow.run_collection import RunCollection
17
-
18
-
19
- class RunCollectionInfo:
20
- """Provide information about MLflow runs."""
21
-
22
- def __init__(self, runs: RunCollection) -> None:
23
- self._runs = runs
24
-
25
- @property
26
- def run_id(self) -> list[str]:
27
- """Get the run ID for each run in the collection."""
28
- return [run.info.run_id for run in self._runs]
29
-
30
- @property
31
- def artifact_uri(self) -> list[str | None]:
32
- """Get the artifact URI for each run in the collection."""
33
- return [run.info.artifact_uri for run in self._runs]
34
-
35
- @property
36
- def artifact_dir(self) -> list[Path]:
37
- """Get the artifact directory for each run in the collection."""
38
- return [get_artifact_dir(run) for run in self._runs]
39
-
40
17
 
41
18
  def get_artifact_dir(run: Run | None = None) -> Path:
42
19
  """Retrieve the artifact directory for the given run.
@@ -89,3 +66,23 @@ def get_hydra_output_dir(run: Run | None = None) -> Path:
89
66
  return Path(hc.hydra.runtime.output_dir)
90
67
 
91
68
  raise FileNotFoundError
69
+
70
+
71
+ def load_config(run: Run) -> DictConfig:
72
+ """Load the configuration for a given run.
73
+
74
+ This function loads the configuration for the provided Run instance
75
+ by downloading the configuration file from the MLflow artifacts and
76
+ loading it using OmegaConf. It returns an empty config if
77
+ `.hydra/config.yaml` is not found in the run's artifact directory.
78
+
79
+ Args:
80
+ run (Run): The Run instance for which to load the configuration.
81
+
82
+ Returns:
83
+ The loaded configuration as a DictConfig object. Returns an empty
84
+ DictConfig if the configuration file is not found.
85
+
86
+ """
87
+ path = get_artifact_dir(run) / ".hydra/config.yaml"
88
+ return OmegaConf.load(path) # type: ignore
@@ -27,6 +27,9 @@ cs.store(name="config", node=MySQLConfig)
27
27
 
28
28
  @hydra.main(version_base=None, config_name="config")
29
29
  def app(cfg: MySQLConfig):
30
+ with hydraflow.chdir_hydra() as path:
31
+ Path("chdir_hydra.txt").write_text(path.as_posix())
32
+
30
33
  hydraflow.set_experiment(prefix="_", suffix="_")
31
34
  with hydraflow.start_run(cfg):
32
35
  log.info(f"START, {cfg.host}, {cfg.port} ")
@@ -34,6 +37,8 @@ def app(cfg: MySQLConfig):
34
37
  artifact_dir = hydraflow.get_artifact_dir()
35
38
  output_dir = hydraflow.get_hydra_output_dir()
36
39
 
40
+ assert (output_dir / "chdir_hydra.txt").exists()
41
+
37
42
  mlflow.log_text("A " + artifact_dir.as_posix(), "artifact_dir.txt")
38
43
  mlflow.log_text("B " + output_dir.as_posix(), "output_dir.txt")
39
44
 
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING
8
8
  import mlflow
9
9
  import pytest
10
10
  from mlflow.entities import RunStatus
11
- from omegaconf import OmegaConf
11
+ from omegaconf import ListConfig, OmegaConf
12
12
 
13
13
  if TYPE_CHECKING:
14
14
  from omegaconf import DictConfig
@@ -114,6 +114,13 @@ def test_app_data_config(rc: RunCollection):
114
114
  assert config[3].host == "y"
115
115
 
116
116
 
117
+ def test_app_data_config_list(rc: RunCollection):
118
+ config = rc.data.config
119
+ assert isinstance(config[0]["values"], ListConfig)
120
+ assert not isinstance(config[0]["values"], list)
121
+ assert config[0]["values"] == [1, 2, 3]
122
+
123
+
117
124
  def test_app_info_artifact_uri(rc: RunCollection):
118
125
  uris = rc.info.artifact_uri
119
126
  assert all(uri.startswith("file://") for uri in uris) # type: ignore
@@ -122,14 +129,14 @@ def test_app_info_artifact_uri(rc: RunCollection):
122
129
 
123
130
 
124
131
  def test_app_info_artifact_dir(rc: RunCollection):
125
- from hydraflow.run_info import get_artifact_dir
132
+ from hydraflow.utils import get_artifact_dir
126
133
 
127
134
  dirs = list(rc.map(get_artifact_dir))
128
135
  assert rc.info.artifact_dir == dirs
129
136
 
130
137
 
131
138
  def test_app_hydra_output_dir(rc: RunCollection):
132
- from hydraflow.run_info import get_hydra_output_dir
139
+ from hydraflow.utils import get_hydra_output_dir
133
140
 
134
141
  dirs = list(rc.map(get_hydra_output_dir))
135
142
  assert dirs[0].stem == "0"
@@ -178,3 +185,10 @@ def test_config(rc: RunCollection):
178
185
  assert df.shape == (4, 3)
179
186
  assert df.select("host").to_series().to_list() == ["x", "x", "y", "y"]
180
187
  assert df.select("port").to_series().to_list() == [1, 2, 1, 2]
188
+ assert str(df.select("values").dtypes) == "[List(Int64)]"
189
+ assert df.select("values").to_series().to_list() == [
190
+ [1, 2, 3],
191
+ [1, 2, 3],
192
+ [1, 2, 3],
193
+ [1, 2, 3],
194
+ ]
@@ -1,56 +0,0 @@
1
- """Provide information about MLflow runs."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import TYPE_CHECKING
6
-
7
- from omegaconf import DictConfig, OmegaConf
8
-
9
- from hydraflow.run_info import get_artifact_dir
10
-
11
- if TYPE_CHECKING:
12
- from mlflow.entities import Run
13
-
14
- from hydraflow.run_collection import RunCollection
15
-
16
-
17
- class RunCollectionData:
18
- """Provide information about MLflow runs."""
19
-
20
- def __init__(self, runs: RunCollection) -> None:
21
- self._runs = runs
22
-
23
- @property
24
- def params(self) -> list[dict[str, str]]:
25
- """Get the parameters for each run in the collection."""
26
- return [run.data.params for run in self._runs]
27
-
28
- @property
29
- def metrics(self) -> list[dict[str, float]]:
30
- """Get the metrics for each run in the collection."""
31
- return [run.data.metrics for run in self._runs]
32
-
33
- @property
34
- def config(self) -> list[DictConfig]:
35
- """Get the configuration for each run in the collection."""
36
- return [load_config(run) for run in self._runs]
37
-
38
-
39
- def load_config(run: Run) -> DictConfig:
40
- """Load the configuration for a given run.
41
-
42
- This function loads the configuration for the provided Run instance
43
- by downloading the configuration file from the MLflow artifacts and
44
- loading it using OmegaConf. It returns an empty config if
45
- `.hydra/config.yaml` is not found in the run's artifact directory.
46
-
47
- Args:
48
- run (Run): The Run instance for which to load the configuration.
49
-
50
- Returns:
51
- The loaded configuration as a DictConfig object. Returns an empty
52
- DictConfig if the configuration file is not found.
53
-
54
- """
55
- path = get_artifact_dir(run) / ".hydra/config.yaml"
56
- return OmegaConf.load(path) # type: ignore
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes