hydraflow 0.7.3__tar.gz → 0.7.5__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. hydraflow-0.7.5/.github/workflows/ci.yaml +51 -0
  2. hydraflow-0.7.5/.github/workflows/docs.yaml +25 -0
  3. {hydraflow-0.7.3 → hydraflow-0.7.5}/PKG-INFO +5 -2
  4. {hydraflow-0.7.3 → hydraflow-0.7.5}/README.md +1 -1
  5. hydraflow-0.7.5/docs/index.md +10 -0
  6. hydraflow-0.7.5/docs/usage/quickstart.md +150 -0
  7. hydraflow-0.7.5/hydraflow.yaml +5 -0
  8. {hydraflow-0.7.3 → hydraflow-0.7.5}/pyproject.toml +12 -17
  9. {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/__init__.py +9 -1
  10. hydraflow-0.7.5/src/hydraflow/cli.py +75 -0
  11. {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/mlflow.py +87 -32
  12. hydraflow-0.7.5/tests/cli/conftest.py +9 -0
  13. hydraflow-0.7.5/tests/cli/test_run.py +18 -0
  14. hydraflow-0.7.5/tests/cli/test_show.py +52 -0
  15. hydraflow-0.7.5/tests/cli/test_version.py +20 -0
  16. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/test_mlflow.py +7 -0
  17. hydraflow-0.7.5/tests/utils/__init__.py +0 -0
  18. {hydraflow-0.7.3 → hydraflow-0.7.5}/.devcontainer/devcontainer.json +0 -0
  19. {hydraflow-0.7.3 → hydraflow-0.7.5}/.devcontainer/postCreate.sh +0 -0
  20. {hydraflow-0.7.3 → hydraflow-0.7.5}/.devcontainer/starship.toml +0 -0
  21. {hydraflow-0.7.3 → hydraflow-0.7.5}/.gitattributes +0 -0
  22. {hydraflow-0.7.3 → hydraflow-0.7.5}/.gitignore +0 -0
  23. {hydraflow-0.7.3 → hydraflow-0.7.5}/LICENSE +0 -0
  24. {hydraflow-0.7.3 → hydraflow-0.7.5}/apps/quickstart.py +0 -0
  25. /hydraflow-0.7.3/mkdocs.yml → /hydraflow-0.7.5/mkdocs.yaml +0 -0
  26. {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/config.py +0 -0
  27. {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/context.py +0 -0
  28. {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/main.py +0 -0
  29. {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/param.py +0 -0
  30. {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/py.typed +0 -0
  31. {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/run_collection.py +0 -0
  32. {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/run_data.py +0 -0
  33. {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/run_info.py +0 -0
  34. {hydraflow-0.7.3 → hydraflow-0.7.5}/src/hydraflow/utils.py +0 -0
  35. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/__init__.py +0 -0
  36. {hydraflow-0.7.3/tests/config → hydraflow-0.7.5/tests/cli}/__init__.py +0 -0
  37. {hydraflow-0.7.3/tests/context → hydraflow-0.7.5/tests/config}/__init__.py +0 -0
  38. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/config/overrides.py +0 -0
  39. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/config/test_config.py +0 -0
  40. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/config/test_overrides.py +0 -0
  41. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/config/test_params.py +0 -0
  42. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/conftest.py +0 -0
  43. {hydraflow-0.7.3/tests/main → hydraflow-0.7.5/tests/context}/__init__.py +0 -0
  44. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/context/chdir.py +0 -0
  45. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/context/context.py +0 -0
  46. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/context/logging.py +0 -0
  47. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/context/rerun.py +0 -0
  48. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/context/test_chdir.py +0 -0
  49. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/context/test_context.py +0 -0
  50. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/context/test_logging.py +0 -0
  51. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/context/test_rerun.py +0 -0
  52. {hydraflow-0.7.3/tests/param → hydraflow-0.7.5/tests/main}/__init__.py +0 -0
  53. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/main/base.py +0 -0
  54. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/main/force_new_run.py +0 -0
  55. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/main/restart.py +0 -0
  56. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/main/skip.py +0 -0
  57. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/main/test_base.py +0 -0
  58. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/main/test_force_new_run.py +0 -0
  59. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/main/test_restart.py +0 -0
  60. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/main/test_skip.py +0 -0
  61. {hydraflow-0.7.3/tests/run → hydraflow-0.7.5/tests/param}/__init__.py +0 -0
  62. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/param/params.py +0 -0
  63. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/param/test_param.py +0 -0
  64. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/param/test_params.py +0 -0
  65. {hydraflow-0.7.3/tests/utils → hydraflow-0.7.5/tests/run}/__init__.py +0 -0
  66. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/run/filter.py +0 -0
  67. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/run/run.py +0 -0
  68. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/run/test_collection.py +0 -0
  69. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/run/test_data.py +0 -0
  70. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/run/test_filter.py +0 -0
  71. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/run/test_info.py +0 -0
  72. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/run/test_run.py +0 -0
  73. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/utils/test_run.py +0 -0
  74. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/utils/test_utils.py +0 -0
  75. {hydraflow-0.7.3 → hydraflow-0.7.5}/tests/utils/utils.py +0 -0
@@ -0,0 +1,51 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ concurrency:
9
+ group: test-${{ github.head_ref }}
10
+ cancel-in-progress: true
11
+
12
+ env:
13
+ PYTHONUNBUFFERED: "1"
14
+ FORCE_COLOR: "1"
15
+
16
+ jobs:
17
+ run:
18
+ name: Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }}
19
+ runs-on: ${{ matrix.os }}
20
+ strategy:
21
+ fail-fast: false
22
+ matrix:
23
+ os: [ubuntu-latest, windows-latest, macos-latest]
24
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
25
+
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ - name: Set up Python ${{ matrix.python-version }}
29
+ uses: actions/setup-python@v5
30
+ with:
31
+ python-version: ${{ matrix.python-version }}
32
+ allow-prereleases: true
33
+ - name: Install uv and ruff
34
+ run: pip install uv ruff
35
+ - name: Install the project
36
+ run: uv sync
37
+ - name: Ruff check
38
+ run: ruff check
39
+ - name: Run test
40
+ run: uv run pytest -v --junitxml=junit.xml
41
+ - name: Upload Codecov Results
42
+ if: success()
43
+ uses: codecov/codecov-action@v4
44
+ with:
45
+ token: ${{ secrets.CODECOV_TOKEN }}
46
+ file: lcov.info
47
+ - name: Upload test results to Codecov
48
+ if: ${{ !cancelled() }}
49
+ uses: codecov/test-results-action@v1
50
+ with:
51
+ token: ${{ secrets.CODECOV_TOKEN }}
@@ -0,0 +1,25 @@
1
+ name: Documentation
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ tags: ["*"]
6
+ permissions:
7
+ contents: write
8
+ jobs:
9
+ deploy:
10
+ name: Documentation
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - name: Configure Git Credentials
15
+ run: |
16
+ git config user.name github-actions[bot]
17
+ git config user.email 41898282+github-actions[bot]@users.noreply.github.com
18
+ - name: Set up Python 3.11
19
+ uses: actions/setup-python@v5
20
+ with:
21
+ python-version: 3.11
22
+ - name: Install package
23
+ run: pip install -e . mkapi markdown-exec[ansi]
24
+ - name: Deploy documentation
25
+ run: mkdocs gh-deploy --force
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hydraflow
3
- Version: 0.7.3
3
+ Version: 0.7.5
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
@@ -38,6 +38,9 @@ Classifier: Programming Language :: Python :: 3.13
38
38
  Requires-Python: >=3.10
39
39
  Requires-Dist: hydra-core>=1.3
40
40
  Requires-Dist: mlflow>=2.15
41
+ Requires-Dist: omegaconf
42
+ Requires-Dist: rich
43
+ Requires-Dist: typer
41
44
  Description-Content-Type: text/markdown
42
45
 
43
46
  # Hydraflow
@@ -52,7 +55,7 @@ Description-Content-Type: text/markdown
52
55
  [pypi-v-link]: https://pypi.org/project/hydraflow/
53
56
  [python-v-image]: https://img.shields.io/pypi/pyversions/hydraflow.svg
54
57
  [python-v-link]: https://pypi.org/project/hydraflow
55
- [GHAction-image]: https://github.com/daizutabi/hydraflow/actions/workflows/ci.yml/badge.svg?branch=main&event=push
58
+ [GHAction-image]: https://github.com/daizutabi/hydraflow/actions/workflows/ci.yaml/badge.svg?branch=main&event=push
56
59
  [GHAction-link]: https://github.com/daizutabi/hydraflow/actions?query=event%3Apush+branch%3Amain
57
60
  [codecov-image]: https://codecov.io/github/daizutabi/hydraflow/coverage.svg?branch=main
58
61
  [codecov-link]: https://codecov.io/github/daizutabi/hydraflow?branch=main
@@ -10,7 +10,7 @@
10
10
  [pypi-v-link]: https://pypi.org/project/hydraflow/
11
11
  [python-v-image]: https://img.shields.io/pypi/pyversions/hydraflow.svg
12
12
  [python-v-link]: https://pypi.org/project/hydraflow
13
- [GHAction-image]: https://github.com/daizutabi/hydraflow/actions/workflows/ci.yml/badge.svg?branch=main&event=push
13
+ [GHAction-image]: https://github.com/daizutabi/hydraflow/actions/workflows/ci.yaml/badge.svg?branch=main&event=push
14
14
  [GHAction-link]: https://github.com/daizutabi/hydraflow/actions?query=event%3Apush+branch%3Amain
15
15
  [codecov-image]: https://codecov.io/github/daizutabi/hydraflow/coverage.svg?branch=main
16
16
  [codecov-link]: https://codecov.io/github/daizutabi/hydraflow?branch=main
@@ -0,0 +1,10 @@
1
+ # Hydraflow Documentation
2
+
3
+ Hydraflow integrates [Hydra](https://hydra.cc/) and [MLflow](https://mlflow.org/)
4
+ to manage and track machine learning experiments.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ pip install hydraflow
10
+ ```
@@ -0,0 +1,150 @@
1
+ # Quickstart
2
+
3
+ ## Hydra application
4
+
5
+ The following example demonstrates how to use Hydraflow with a Hydra application.
6
+ There are two main steps to using Hydraflow:
7
+
8
+ 1. Set the MLflow experiment using the Hydra job name.
9
+ 2. Start a new MLflow run that logs the Hydra configuration.
10
+
11
+ ```python title="apps/quickstart.py" linenums="1" hl_lines="24 26"
12
+ --8<-- "apps/quickstart.py"
13
+ ```
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
+ ### Start a new MLflow run
26
+
27
+ [`hydraflow.start_run`][] starts a new MLflow run that logs the Hydra configuration.
28
+ It returns the started run so that it can be used to log metrics, parameters, and artifacts
29
+ within the context of the run.
30
+
31
+ ```python
32
+ with hydraflow.start_run(cfg) as run:
33
+ pass
34
+ ```
35
+
36
+ ## Run the application
37
+
38
+ ```bash exec="on"
39
+ rm -rf mlruns outputs multirun
40
+ ```
41
+
42
+ ### Single-run
43
+
44
+ Run the Hydra application as a normal Python script.
45
+
46
+ ```console exec="1" source="console"
47
+ $ python apps/quickstart.py
48
+ ```
49
+
50
+ Check the MLflow CLI to view the experiment.
51
+
52
+ ```console exec="1" source="console"
53
+ $ mlflow experiments search
54
+ ```
55
+
56
+ ### Multi-run
57
+
58
+ ```console exec="1" source="console"
59
+ $ python apps/quickstart.py -m width=400,600 height=100,200,300
60
+ ```
61
+
62
+ ## Use Hydraflow API
63
+
64
+ ### Run collection
65
+
66
+ ```pycon exec="1" source="console" session="quickstart"
67
+ >>> import mlflow
68
+ >>> mlflow.set_experiment("quickstart")
69
+ >>> import hydraflow
70
+ >>> rc = hydraflow.list_runs()
71
+ >>> print(rc)
72
+ ```
73
+
74
+ ### Retrieve a run
75
+
76
+ ```pycon exec="1" source="console" session="quickstart"
77
+ >>> run = rc.first()
78
+ >>> print(type(run))
79
+ ```
80
+
81
+ ```pycon exec="1" source="console" session="quickstart"
82
+ >>> cfg = hydraflow.load_config(run)
83
+ >>> print(type(cfg))
84
+ >>> print(cfg)
85
+ ```
86
+
87
+ ```pycon exec="1" source="console" session="quickstart"
88
+ >>> run = rc.last()
89
+ >>> cfg = hydraflow.load_config(run)
90
+ >>> print(cfg)
91
+ ```
92
+
93
+ ### Filter runs
94
+
95
+ ```pycon exec="1" source="console" session="quickstart"
96
+ >>> filtered = rc.filter(width=400)
97
+ >>> print(filtered)
98
+ ```
99
+
100
+ ```pycon exec="1" source="console" session="quickstart"
101
+ >>> filtered = rc.filter(height=[100, 300])
102
+ >>> print(filtered)
103
+ ```
104
+
105
+ ```pycon exec="1" source="console" session="quickstart"
106
+ >>> filtered = rc.filter(height=(100, 300))
107
+ >>> print(filtered)
108
+ ```
109
+
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
+ ### Map runs
121
+
122
+ ```pycon exec="1" source="console" session="quickstart"
123
+ >>> params = rc.map(lambda x: x.data.params)
124
+ >>> for p in params:
125
+ ... print(p)
126
+ ```
127
+
128
+ ```pycon exec="1" source="console" session="quickstart"
129
+ >>> list(rc.map_id(print))
130
+ ```
131
+
132
+ ### Group runs
133
+
134
+ ```pycon exec="1" source="console" session="quickstart"
135
+ >>> grouped = rc.groupby("width")
136
+ >>> for key, group in grouped.items():
137
+ ... print(key, group)
138
+ ```
139
+
140
+ ```pycon exec="1" source="console" session="quickstart"
141
+ >>> grouped = rc.groupby(["height"])
142
+ >>> for key, group in grouped.items():
143
+ ... print(key, group)
144
+ ```
145
+
146
+ ### Config dataframe
147
+
148
+ ```pycon exec="1" source="console" session="quickstart"
149
+ >>> print(rc.data.config)
150
+ ```
@@ -0,0 +1,5 @@
1
+ a:
2
+ b: [1, 2]
3
+ c.d: 3m
4
+ c.e/3: 4
5
+ e: true
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
- requires = ["hatchling>=1.26.1"]
2
+ requires = ["hatchling"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hydraflow"
7
- version = "0.7.3"
7
+ version = "0.7.5"
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,40 +19,34 @@ 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"]
22
+ dependencies = ["hydra-core>=1.3", "mlflow>=2.15", "omegaconf", "rich", "typer"]
23
23
 
24
24
  [project.urls]
25
25
  Documentation = "https://daizutabi.github.io/hydraflow/"
26
26
  Source = "https://github.com/daizutabi/hydraflow"
27
27
  Issues = "https://github.com/daizutabi/hydraflow/issues"
28
28
 
29
- [tool.uv]
30
- dev-dependencies = [
31
- "markdown-exec[ansi]",
32
- "mkapi",
33
- "mkdocs-material",
34
- "mkdocs>=1.6",
29
+ [project.scripts]
30
+ hydraflow = "hydraflow.cli:app"
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ "pytest-clarity",
35
35
  "pytest-cov",
36
36
  "pytest-order",
37
37
  "pytest-randomly",
38
38
  "pytest-xdist",
39
39
  ]
40
-
41
- [tool.hatch.build.targets.sdist]
42
- exclude = ["/.github", "/docs"]
43
-
44
- [tool.hatch.build.targets.wheel]
45
- packages = ["src/hydraflow"]
40
+ docs = ["markdown-exec[ansi]", "mkapi", "mkdocs-material"]
46
41
 
47
42
  [tool.pytest.ini_options]
48
43
  addopts = [
49
44
  "--cov=hydraflow",
50
45
  "--cov-report=lcov:lcov.info",
51
- "-n8",
52
46
  "--dist=loadgroup",
47
+ "-n8",
53
48
  ]
54
49
  filterwarnings = [
55
- "ignore:pkg_resources is deprecated:DeprecationWarning",
56
50
  "ignore:Support for class-based `config` is deprecated",
57
51
  "ignore:Pydantic V1 style",
58
52
  ]
@@ -98,3 +92,4 @@ ignore = [
98
92
  ]
99
93
  "apps/*.py" = ["D", "G", "INP"]
100
94
  "src/hydraflow/main.py" = ["ANN201", "D401", "PLR0913"]
95
+ "src/hydraflow/cli.py" = ["ANN", "D"]
@@ -3,7 +3,13 @@
3
3
  from hydraflow.config import select_config, select_overrides
4
4
  from hydraflow.context import chdir_artifact, log_run, start_run
5
5
  from hydraflow.main import main
6
- from hydraflow.mlflow import list_runs, search_runs, set_experiment
6
+ from hydraflow.mlflow import (
7
+ list_run_ids,
8
+ list_run_paths,
9
+ list_runs,
10
+ search_runs,
11
+ set_experiment,
12
+ )
7
13
  from hydraflow.run_collection import RunCollection
8
14
  from hydraflow.utils import (
9
15
  get_artifact_dir,
@@ -22,6 +28,8 @@ __all__ = [
22
28
  "get_artifact_path",
23
29
  "get_hydra_output_dir",
24
30
  "get_overrides",
31
+ "list_run_ids",
32
+ "list_run_paths",
25
33
  "list_runs",
26
34
  "load_config",
27
35
  "load_overrides",
@@ -0,0 +1,75 @@
1
+ """Hydraflow CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from omegaconf import DictConfig, OmegaConf
10
+ from rich.console import Console
11
+ from typer import Argument, Option
12
+
13
+ app = typer.Typer(add_completion=False)
14
+ console = Console()
15
+
16
+
17
+ @app.command()
18
+ def run(
19
+ names: Annotated[
20
+ list[str] | None,
21
+ Argument(help="Job names.", show_default=False),
22
+ ] = None,
23
+ ) -> None:
24
+ """Run jobs."""
25
+ typer.echo(names)
26
+
27
+ cfg = load_config()
28
+ typer.echo(cfg)
29
+
30
+
31
+ @app.command()
32
+ def show() -> None:
33
+ """Show the config."""
34
+ from rich.syntax import Syntax
35
+
36
+ cfg = load_config()
37
+ code = OmegaConf.to_yaml(cfg)
38
+ syntax = Syntax(code, "yaml")
39
+ console.print(syntax)
40
+
41
+
42
+ @app.callback(invoke_without_command=True)
43
+ def callback(
44
+ *,
45
+ version: Annotated[
46
+ bool,
47
+ Option("--version", help="Show the version and exit."),
48
+ ] = False,
49
+ ) -> None:
50
+ if version:
51
+ import importlib.metadata
52
+
53
+ typer.echo(f"hydraflow {importlib.metadata.version('hydraflow')}")
54
+ raise typer.Exit
55
+
56
+
57
+ def find_config() -> Path:
58
+ if Path("hydraflow.yaml").exists():
59
+ return Path("hydraflow.yaml")
60
+
61
+ if Path("hydraflow.yml").exists():
62
+ return Path("hydraflow.yml")
63
+
64
+ typer.echo("No config file found.")
65
+ raise typer.Exit(code=1)
66
+
67
+
68
+ def load_config() -> DictConfig:
69
+ cfg = OmegaConf.load(find_config())
70
+
71
+ if isinstance(cfg, DictConfig):
72
+ return cfg
73
+
74
+ typer.echo("Invalid config file.")
75
+ raise typer.Exit(code=1)
@@ -153,18 +153,17 @@ def search_runs( # noqa: PLR0913
153
153
  return RunCollection(runs) # type: ignore
154
154
 
155
155
 
156
- def list_runs(
156
+ def list_run_paths(
157
157
  experiment_names: str | list[str] | None = None,
158
- n_jobs: int = 0,
159
- status: str | list[str] | int | list[int] | None = None,
160
- ) -> RunCollection:
161
- """List all runs for the specified experiments.
158
+ *other: str,
159
+ ) -> list[Path]:
160
+ """List all run paths for the specified experiments.
162
161
 
163
- This function retrieves all runs for the given list of experiment names.
162
+ This function retrieves all run paths for the given list of experiment names.
164
163
  If no experiment names are provided (None), it defaults to searching all runs
165
164
  for the currently active experiment. If an empty list is provided, the function
166
165
  will search all runs for all experiments except the "Default" experiment.
167
- The function returns the results as a `RunCollection` object.
166
+ The function returns the results as a list of `Path` objects.
168
167
 
169
168
  Note:
170
169
  The returned runs are sorted by their start time in ascending order.
@@ -174,27 +173,12 @@ def list_runs(
174
173
  for runs. If None or an empty list is provided, the function will
175
174
  search the currently active experiment or all experiments except
176
175
  the "Default" experiment.
177
- n_jobs (int): The number of jobs to run in parallel. If 0, the function
178
- will search runs sequentially.
179
- status (str | list[str] | int | list[int] | None): The status of the runs
180
- to filter.
176
+ other (str): The parts of the run directory to join.
181
177
 
182
178
  Returns:
183
- RunCollection: A `RunCollection` instance containing the runs for the
184
- specified experiments.
179
+ list[Path]: A list of run paths for the specified experiments.
185
180
 
186
181
  """
187
- rc = _list_runs(experiment_names, n_jobs)
188
- if status is None:
189
- return rc
190
-
191
- return rc.filter(status=status)
192
-
193
-
194
- def _list_runs(
195
- experiment_names: str | list[str] | None = None,
196
- n_jobs: int = 0,
197
- ) -> RunCollection:
198
182
  if isinstance(experiment_names, str):
199
183
  experiment_names = [experiment_names]
200
184
 
@@ -202,14 +186,11 @@ def _list_runs(
202
186
  experiments = mlflow.search_experiments()
203
187
  experiment_names = [e.name for e in experiments if e.name != "Default"]
204
188
 
205
- if n_jobs == 0:
206
- return search_runs(experiment_names=experiment_names)
207
-
208
189
  if experiment_names is None:
209
190
  experiment_id = _get_experiment_id()
210
191
  experiment_names = [mlflow.get_experiment(experiment_id).name]
211
192
 
212
- run_ids = []
193
+ run_paths: list[Path] = []
213
194
 
214
195
  for name in experiment_names:
215
196
  if experiment := mlflow.get_experiment_by_name(name):
@@ -217,9 +198,83 @@ def _list_runs(
217
198
 
218
199
  if isinstance(uri, str):
219
200
  path = get_artifact_dir(uri=uri)
220
- run_ids.extend(file.stem for file in path.iterdir() if file.is_dir())
201
+ run_paths.extend(p for p in path.iterdir() if p.is_dir())
202
+
203
+ if other:
204
+ return [p.joinpath(*other) for p in run_paths]
205
+
206
+ return run_paths
207
+
208
+
209
+ def list_run_ids(experiment_names: str | list[str] | None = None) -> list[str]:
210
+ """List all run IDs for the specified experiments.
211
+
212
+ This function retrieves all runs for the given list of experiment names.
213
+ If no experiment names are provided (None), it defaults to searching all runs
214
+ for the currently active experiment. If an empty list is provided, the function
215
+ will search all runs for all experiments except the "Default" experiment.
216
+ The function returns the results as a list of string.
217
+
218
+ Note:
219
+ The returned runs are sorted by their start time in ascending order.
220
+
221
+ Args:
222
+ experiment_names (list[str] | None): List of experiment names to search
223
+ for runs. If None or an empty list is provided, the function will
224
+ search the currently active experiment or all experiments except
225
+ the "Default" experiment.
226
+
227
+ Returns:
228
+ list[str]: A list of run IDs for the specified experiments.
229
+
230
+ """
231
+ return [run_dir.stem for run_dir in list_run_paths(experiment_names)]
232
+
233
+
234
+ def list_runs(
235
+ experiment_names: str | list[str] | None = None,
236
+ n_jobs: int = 0,
237
+ status: str | list[str] | int | list[int] | None = None,
238
+ ) -> RunCollection:
239
+ """List all runs for the specified experiments.
240
+
241
+ This function retrieves all runs for the given list of experiment names.
242
+ If no experiment names are provided (None), it defaults to searching all runs
243
+ for the currently active experiment. If an empty list is provided, the function
244
+ will search all runs for all experiments except the "Default" experiment.
245
+ The function returns the results as a `RunCollection` object.
246
+
247
+ Note:
248
+ The returned runs are sorted by their start time in ascending order.
249
+
250
+ Args:
251
+ experiment_names (list[str] | None): List of experiment names to search
252
+ for runs. If None or an empty list is provided, the function will
253
+ search the currently active experiment or all experiments except
254
+ the "Default" experiment.
255
+ n_jobs (int): The number of jobs to run in parallel. If 0, the function
256
+ will search runs sequentially.
257
+ status (str | list[str] | int | list[int] | None): The status of the runs
258
+ to filter.
259
+
260
+ Returns:
261
+ RunCollection: A `RunCollection` instance containing the runs for the
262
+ specified experiments.
263
+
264
+ """
265
+ run_ids = list_run_ids(experiment_names)
266
+
267
+ if n_jobs == 0:
268
+ runs = [mlflow.get_run(run_id) for run_id in run_ids]
269
+
270
+ else:
271
+ it = (joblib.delayed(mlflow.get_run)(run_id) for run_id in run_ids)
272
+ runs = joblib.Parallel(n_jobs, prefer="threads")(it)
221
273
 
222
- it = (joblib.delayed(mlflow.get_run)(run_id) for run_id in run_ids)
223
- runs = joblib.Parallel(n_jobs, prefer="threads")(it)
224
274
  runs = sorted(runs, key=lambda run: run.info.start_time) # type: ignore
225
- return RunCollection(runs) # type: ignore
275
+ rc = RunCollection(runs) # type: ignore
276
+
277
+ if status is None:
278
+ return rc
279
+
280
+ return rc.filter(status=status)
@@ -0,0 +1,9 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+
6
+ @pytest.fixture(autouse=True)
7
+ def setup(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
8
+ monkeypatch.chdir(tmp_path)
9
+ yield
@@ -0,0 +1,18 @@
1
+ from pathlib import Path
2
+
3
+ from typer.testing import CliRunner
4
+
5
+ from hydraflow.cli import app
6
+
7
+ runner = CliRunner()
8
+
9
+
10
+ def test_invoke_error():
11
+ result = runner.invoke(app, ["run"])
12
+ assert result.exit_code == 1
13
+
14
+
15
+ def test_invoke():
16
+ Path("hydraflow.yaml").write_text("a:\n b: [1, 2]")
17
+ result = runner.invoke(app, ["run"])
18
+ assert result.exit_code == 0
@@ -0,0 +1,52 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+ import typer
5
+ from typer.testing import CliRunner
6
+
7
+ from hydraflow.cli import app
8
+
9
+ runner = CliRunner()
10
+
11
+
12
+ @pytest.mark.parametrize("file", ["hydraflow.yaml", "hydraflow.yml"])
13
+ def test_find_config(file):
14
+ from hydraflow.cli import find_config
15
+
16
+ Path(file).touch()
17
+ assert find_config() == Path(file)
18
+
19
+
20
+ def test_find_config_error():
21
+ from hydraflow.cli import find_config
22
+
23
+ with pytest.raises(typer.Exit):
24
+ find_config()
25
+
26
+
27
+ def test_load_config():
28
+ from hydraflow.cli import load_config
29
+
30
+ Path("hydraflow.yaml").write_text("a:\n b: 1")
31
+ cfg = load_config()
32
+ assert cfg["a"]["b"] == 1
33
+
34
+
35
+ def test_load_config_error():
36
+ from hydraflow.cli import load_config
37
+
38
+ Path("hydraflow.yml").write_text("- 1\n- 2")
39
+
40
+ with pytest.raises(typer.Exit):
41
+ load_config()
42
+
43
+
44
+ def test_invoke_error():
45
+ result = runner.invoke(app, ["show"])
46
+ assert result.exit_code == 1
47
+
48
+
49
+ def test_invoke():
50
+ Path("hydraflow.yaml").write_text("a:\n b: [1, 2]")
51
+ result = runner.invoke(app, ["show"])
52
+ assert result.exit_code == 0
@@ -0,0 +1,20 @@
1
+ import subprocess
2
+
3
+ from typer.testing import CliRunner
4
+
5
+ from hydraflow.cli import app
6
+
7
+ runner = CliRunner()
8
+
9
+
10
+ def test_invoke():
11
+ result = runner.invoke(app, ["--version"])
12
+ assert result.exit_code == 0
13
+ assert "hydraflow" in result.stdout
14
+ assert result.stdout.count(".") == 2
15
+
16
+
17
+ def test_command():
18
+ out = subprocess.check_output(["hydraflow", "--version"], text=True)
19
+ assert "hydraflow" in out
20
+ assert out.count(".") == 2
@@ -92,3 +92,10 @@ def test_list_runs(experiment: Experiment, status, n, n_jobs, func):
92
92
  experiment_names = func(experiment.name)
93
93
  rc = list_runs(experiment_names=experiment_names, status=status, n_jobs=n_jobs)
94
94
  assert len(rc) == n
95
+
96
+
97
+ def test_list_run_dirs(experiment: Experiment):
98
+ from hydraflow.mlflow import list_run_paths
99
+
100
+ dirs = list_run_paths(experiment.name, "artifacts")
101
+ assert all(d.is_dir() for d in dirs)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes