hydraflow 0.7.5__py3-none-any.whl → 0.8.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
hydraflow/__init__.py CHANGED
@@ -1,23 +1,14 @@
1
1
  """Integrate Hydra and MLflow to manage and track machine learning experiments."""
2
2
 
3
- from hydraflow.config import select_config, select_overrides
4
3
  from hydraflow.context import chdir_artifact, log_run, start_run
5
4
  from hydraflow.main import main
6
- from hydraflow.mlflow import (
7
- list_run_ids,
8
- list_run_paths,
9
- list_runs,
10
- search_runs,
11
- set_experiment,
12
- )
5
+ from hydraflow.mlflow import list_run_ids, list_run_paths, list_runs
13
6
  from hydraflow.run_collection import RunCollection
14
7
  from hydraflow.utils import (
15
8
  get_artifact_dir,
16
9
  get_artifact_path,
17
10
  get_hydra_output_dir,
18
- get_overrides,
19
11
  load_config,
20
- load_overrides,
21
12
  remove_run,
22
13
  )
23
14
 
@@ -27,18 +18,12 @@ __all__ = [
27
18
  "get_artifact_dir",
28
19
  "get_artifact_path",
29
20
  "get_hydra_output_dir",
30
- "get_overrides",
31
21
  "list_run_ids",
32
22
  "list_run_paths",
33
23
  "list_runs",
34
24
  "load_config",
35
- "load_overrides",
36
25
  "log_run",
37
26
  "main",
38
27
  "remove_run",
39
- "search_runs",
40
- "select_config",
41
- "select_overrides",
42
- "set_experiment",
43
28
  "start_run",
44
29
  ]
hydraflow/config.py CHANGED
@@ -6,35 +6,19 @@ from typing import TYPE_CHECKING
6
6
 
7
7
  from omegaconf import DictConfig, ListConfig, OmegaConf
8
8
 
9
- from hydraflow.utils import get_overrides
10
-
11
9
  if TYPE_CHECKING:
12
10
  from collections.abc import Iterator
13
11
  from typing import Any
14
12
 
15
13
 
16
- def collect_params(config: object) -> dict[str, Any]:
17
- """Iterate over parameters and collect them into a dictionary.
18
-
19
- Args:
20
- config (object): The configuration object to iterate over.
21
- prefix (str): The prefix to prepend to the parameter keys.
22
-
23
- Returns:
24
- dict[str, Any]: A dictionary of collected parameters.
25
-
26
- """
27
- return dict(iter_params(config))
28
-
29
-
30
- def iter_params(config: object, prefix: str = "") -> Iterator[tuple[str, Any]]:
14
+ def iter_params(config: Any, prefix: str = "") -> Iterator[tuple[str, Any]]:
31
15
  """Recursively iterate over the parameters in the given configuration object.
32
16
 
33
17
  This function traverses the configuration object and yields key-value pairs
34
18
  representing the parameters. The keys are prefixed with the provided prefix.
35
19
 
36
20
  Args:
37
- config (object): The configuration object to iterate over. This can be a
21
+ config (Any): The configuration object to iterate over. This can be a
38
22
  dictionary, list, DictConfig, or ListConfig.
39
23
  prefix (str): The prefix to prepend to the parameter keys.
40
24
  Defaults to an empty string.
@@ -50,7 +34,7 @@ def iter_params(config: object, prefix: str = "") -> Iterator[tuple[str, Any]]:
50
34
  config = _from_dotlist(config)
51
35
 
52
36
  if not isinstance(config, DictConfig | ListConfig):
53
- config = OmegaConf.create(config) # type: ignore
37
+ config = OmegaConf.create(config)
54
38
 
55
39
  yield from _iter_params(config, prefix)
56
40
 
@@ -65,7 +49,7 @@ def _from_dotlist(config: list[str]) -> dict[str, str]:
65
49
  return result
66
50
 
67
51
 
68
- def _iter_params(config: object, prefix: str = "") -> Iterator[tuple[str, Any]]:
52
+ def _iter_params(config: Any, prefix: str = "") -> Iterator[tuple[str, Any]]:
69
53
  if isinstance(config, DictConfig):
70
54
  for key, value in config.items():
71
55
  if _is_param(value):
@@ -83,12 +67,12 @@ def _iter_params(config: object, prefix: str = "") -> Iterator[tuple[str, Any]]:
83
67
  yield from _iter_params(value, f"{prefix}{index}.")
84
68
 
85
69
 
86
- def _is_param(value: object) -> bool:
70
+ def _is_param(value: Any) -> bool:
87
71
  """Check if the given value is a parameter."""
88
72
  if isinstance(value, DictConfig):
89
73
  return False
90
74
 
91
- if isinstance(value, ListConfig): # noqa: SIM102
75
+ if isinstance(value, ListConfig):
92
76
  if any(isinstance(v, DictConfig | ListConfig) for v in value):
93
77
  return False
94
78
 
@@ -103,14 +87,14 @@ def _convert(value: Any) -> Any:
103
87
  return value
104
88
 
105
89
 
106
- def select_config(config: object, names: list[str]) -> dict[str, Any]:
90
+ def select_config(config: Any, names: list[str]) -> dict[str, Any]:
107
91
  """Select the given parameters from the configuration object.
108
92
 
109
93
  This function selects the given parameters from the configuration object
110
94
  and returns a new configuration object containing only the selected parameters.
111
95
 
112
96
  Args:
113
- config (object): The configuration object to select parameters from.
97
+ config (Any): The configuration object to select parameters from.
114
98
  names (list[str]): The names of the parameters to select.
115
99
 
116
100
  Returns:
@@ -120,7 +104,7 @@ def select_config(config: object, names: list[str]) -> dict[str, Any]:
120
104
  if not isinstance(config, DictConfig):
121
105
  config = OmegaConf.structured(config)
122
106
 
123
- return {name: _get(config, name) for name in names} # type: ignore
107
+ return {name: _get(config, name) for name in names}
124
108
 
125
109
 
126
110
  def _get(config: DictConfig, name: str) -> Any:
@@ -132,8 +116,7 @@ def _get(config: DictConfig, name: str) -> Any:
132
116
  return _get(config.get(prefix), name)
133
117
 
134
118
 
135
- def select_overrides(config: object) -> dict[str, Any]:
119
+ def select_overrides(config: object, overrides: list[str]) -> dict[str, Any]:
136
120
  """Select the given overrides from the configuration object."""
137
- overrides = get_overrides()
138
121
  names = [override.split("=")[0].strip() for override in overrides]
139
122
  return select_config(config, names)
hydraflow/context.py CHANGED
@@ -12,7 +12,7 @@ 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
15
+ from hydraflow.mlflow import log_params, log_text
16
16
  from hydraflow.utils import get_artifact_dir
17
17
 
18
18
  if TYPE_CHECKING:
@@ -55,11 +55,11 @@ def log_run(
55
55
  log_params(config, synchronous=synchronous)
56
56
 
57
57
  hc = HydraConfig.get()
58
- output_dir = Path(hc.runtime.output_dir)
58
+ hydra_dir = Path(hc.runtime.output_dir)
59
59
 
60
60
  # Save '.hydra' config directory.
61
- output_subdir = output_dir / (hc.output_subdir or "")
62
- mlflow.log_artifacts(output_subdir.as_posix(), hc.output_subdir)
61
+ hydra_subdir = hydra_dir / (hc.output_subdir or "")
62
+ mlflow.log_artifacts(hydra_subdir.as_posix(), hc.output_subdir)
63
63
 
64
64
  try:
65
65
  yield
@@ -70,43 +70,14 @@ def log_run(
70
70
  raise
71
71
 
72
72
  finally:
73
- log_text(output_dir)
74
-
75
-
76
- def log_text(directory: Path, pattern: str = "*.log") -> None:
77
- """Log text files in the given directory as artifacts.
78
-
79
- Append the text files to the existing text file in the artifact directory.
80
-
81
- Args:
82
- directory (Path): The directory to find the logs in.
83
- pattern (str): The pattern to match the logs.
84
-
85
- """
86
- artifact_dir = get_artifact_dir()
87
-
88
- for file in directory.glob(pattern):
89
- if not file.is_file():
90
- continue
91
-
92
- file_artifact = artifact_dir / file.name
93
- if file_artifact.exists():
94
- text = file_artifact.read_text()
95
- if not text.endswith("\n"):
96
- text += "\n"
97
- else:
98
- text = ""
99
-
100
- text += file.read_text()
101
- mlflow.log_text(text, file.name)
73
+ log_text(hydra_dir)
102
74
 
103
75
 
104
76
  @contextmanager
105
- def start_run( # noqa: PLR0913
77
+ def start_run(
106
78
  config: object,
107
79
  *,
108
80
  chdir: bool = False,
109
- run: Run | None = None,
110
81
  run_id: str | None = None,
111
82
  experiment_id: str | None = None,
112
83
  run_name: str | None = None,
@@ -126,7 +97,6 @@ def start_run( # noqa: PLR0913
126
97
  config (object): The configuration object to log parameters from.
127
98
  chdir (bool): Whether to change the current working directory to the
128
99
  artifact directory of the current run. Defaults to False.
129
- run (Run | None): The existing run. Defaults to None.
130
100
  run_id (str | None): The existing run ID. Defaults to None.
131
101
  experiment_id (str | None): The experiment ID. Defaults to None.
132
102
  run_name (str | None): The name of the run. Defaults to None.
@@ -142,20 +112,7 @@ def start_run( # noqa: PLR0913
142
112
  Yields:
143
113
  Run: An MLflow Run object representing the started run.
144
114
 
145
- Example:
146
- with start_run(config) as run:
147
- # Perform operations within the MLflow run context
148
- pass
149
-
150
- See Also:
151
- - `mlflow.start_run`: The MLflow function to start a run directly.
152
- - `log_run`: A context manager to log parameters and manage the MLflow
153
- run context.
154
-
155
115
  """
156
- if run:
157
- run_id = run.info.run_id
158
-
159
116
  with (
160
117
  mlflow.start_run(
161
118
  run_id=run_id,
hydraflow/main.py CHANGED
@@ -1,54 +1,162 @@
1
- """main decorator."""
1
+ """Integration of MLflow experiment tracking with Hydra configuration management.
2
+
3
+ This module provides decorators and utilities to seamlessly combine Hydra's
4
+ configuration management with MLflow's experiment tracking capabilities. It
5
+ enables automatic run deduplication, configuration storage, and experiment
6
+ management.
7
+
8
+ The main functionality is provided through the `main` decorator, which can be
9
+ used to wrap experiment entry points. This decorator handles:
10
+ - Configuration management via Hydra
11
+ - Experiment tracking via MLflow
12
+ - Run deduplication based on configurations
13
+ - Working directory management
14
+ - Automatic configuration storage
15
+
16
+ Example:
17
+ ```python
18
+ from dataclasses import dataclass
19
+ from mlflow.entities import Run
20
+
21
+ @dataclass
22
+ class Config:
23
+ learning_rate: float
24
+ batch_size: int
25
+
26
+ @main(Config)
27
+ def train(run: Run, config: Config):
28
+ # Your training code here
29
+ pass
30
+ ```
31
+
32
+ """
2
33
 
3
34
  from __future__ import annotations
4
35
 
5
36
  from functools import wraps
6
- from typing import TYPE_CHECKING, Any
37
+ from typing import TYPE_CHECKING, TypeVar
7
38
 
8
39
  import hydra
40
+ import mlflow
9
41
  from hydra.core.config_store import ConfigStore
42
+ from hydra.core.hydra_config import HydraConfig
10
43
  from mlflow.entities import RunStatus
44
+ from omegaconf import OmegaConf
11
45
 
12
46
  import hydraflow
47
+ from hydraflow.utils import file_uri_to_path
13
48
 
14
49
  if TYPE_CHECKING:
15
50
  from collections.abc import Callable
51
+ from pathlib import Path
16
52
 
17
53
  from mlflow.entities import Run
18
54
 
19
55
  FINISHED = RunStatus.to_string(RunStatus.FINISHED)
20
56
 
57
+ T = TypeVar("T")
58
+
21
59
 
22
60
  def main(
23
- node: Any,
61
+ node: T | type[T],
24
62
  config_name: str = "config",
25
63
  *,
26
64
  chdir: bool = False,
27
65
  force_new_run: bool = False,
28
- skip_finished: bool = True,
66
+ match_overrides: bool = False,
67
+ rerun_finished: bool = False,
29
68
  ):
30
- """Main decorator."""
31
-
32
- def decorator(app: Callable[[Run, Any], None]) -> Callable[[], None]:
33
- ConfigStore.instance().store(name=config_name, node=node)
34
-
69
+ """Decorator for configuring and running MLflow experiments with Hydra.
70
+
71
+ This decorator combines Hydra configuration management with MLflow experiment
72
+ tracking. It automatically handles run deduplication and configuration storage.
73
+
74
+ Args:
75
+ node: Configuration node class or instance defining the structure of the
76
+ configuration.
77
+ config_name: Name of the configuration. Defaults to "config".
78
+ chdir: If True, changes working directory to the artifact directory
79
+ of the run. Defaults to False.
80
+ force_new_run: If True, always creates a new MLflow run instead of
81
+ reusing existing ones. Defaults to False.
82
+ match_overrides: If True, matches runs based on Hydra CLI overrides
83
+ instead of full config. Defaults to False.
84
+ rerun_finished: If True, allows rerunning completed runs. Defaults to
85
+ False.
86
+
87
+ """
88
+
89
+ def decorator(app: Callable[[Run, T], None]) -> Callable[[], None]:
90
+ ConfigStore.instance().store(config_name, node)
91
+
92
+ @hydra.main(config_name=config_name, version_base=None)
35
93
  @wraps(app)
36
- @hydra.main(version_base=None, config_name=config_name)
37
- def inner_app(cfg: object) -> None:
38
- hydraflow.set_experiment()
94
+ def inner_decorator(config: T) -> None:
95
+ hc = HydraConfig.get()
96
+ experiment = mlflow.set_experiment(hc.job.name)
39
97
 
40
98
  if force_new_run:
41
- run = None
99
+ run_id = None
42
100
  else:
43
- rc = hydraflow.search_runs()
44
- run = rc.try_get(cfg, override=True)
101
+ uri = experiment.artifact_location
102
+ overrides = hc.overrides.task if match_overrides else None
103
+ run_id = get_run_id(uri, config, overrides)
45
104
 
46
- if skip_finished and run and run.info.status == FINISHED:
47
- return
105
+ if run_id and not rerun_finished:
106
+ run = mlflow.get_run(run_id)
107
+ if run.info.status == FINISHED:
108
+ return
48
109
 
49
- with hydraflow.start_run(cfg, run=run, chdir=chdir) as run:
50
- app(run, cfg)
110
+ with hydraflow.start_run(config, run_id=run_id, chdir=chdir) as run:
111
+ app(run, config)
51
112
 
52
- return inner_app
113
+ return inner_decorator
53
114
 
54
115
  return decorator
116
+
117
+
118
+ def get_run_id(uri: str, config: object, overrides: list[str] | None) -> str | None:
119
+ """Try to get the run ID for the given configuration.
120
+
121
+ If the run is not found, the function will return None.
122
+
123
+ Args:
124
+ uri (str): The URI of the experiment.
125
+ config (object): The configuration object.
126
+ overrides (list[str] | None): The task overrides.
127
+
128
+ Returns:
129
+ The run ID for the given configuration or overrides. Returns None if
130
+ no run ID is found.
131
+
132
+ """
133
+ for run_dir in file_uri_to_path(uri).iterdir():
134
+ if run_dir.is_dir() and equals(run_dir, config, overrides):
135
+ return run_dir.name
136
+
137
+ return None
138
+
139
+
140
+ def equals(run_dir: Path, config: object, overrides: list[str] | None) -> bool:
141
+ """Check if the run directory matches the given configuration or overrides.
142
+
143
+ Args:
144
+ run_dir (Path): The run directory.
145
+ config (object): The configuration object.
146
+ overrides (list[str] | None): The task overrides.
147
+
148
+ Returns:
149
+ True if the run directory matches the given configuration or overrides,
150
+ False otherwise.
151
+
152
+ """
153
+ if overrides is None:
154
+ path = run_dir / "artifacts/.hydra/config.yaml"
155
+ else:
156
+ path = run_dir / "artifacts/.hydra/overrides.yaml"
157
+ config = overrides
158
+
159
+ if not path.exists():
160
+ return False
161
+
162
+ return OmegaConf.load(path) == config
hydraflow/mlflow.py CHANGED
@@ -1,17 +1,8 @@
1
- """Provide functionality to log parameters from Hydra configuration objects.
1
+ """Integration of MLflow experiment tracking with Hydra configuration management.
2
2
 
3
3
  This module provides functions to log parameters from Hydra configuration objects
4
4
  to MLflow, set experiments, and manage tracking URIs. It integrates Hydra's
5
5
  configuration management with MLflow's experiment tracking capabilities.
6
-
7
- Key Features:
8
- - **Experiment Management**: Set experiment names and tracking URIs using Hydra
9
- configuration details.
10
- - **Parameter Logging**: Log parameters from Hydra configuration objects to MLflow,
11
- supporting both synchronous and asynchronous logging.
12
- - **Run Collection**: Utilize the `RunCollection` class to manage and interact with
13
- multiple MLflow runs, providing methods to filter and retrieve runs based on
14
- various criteria.
15
6
  """
16
7
 
17
8
  from __future__ import annotations
@@ -21,55 +12,17 @@ from typing import TYPE_CHECKING
21
12
  import joblib
22
13
  import mlflow
23
14
  import mlflow.artifacts
24
- from hydra.core.hydra_config import HydraConfig
25
- from mlflow.entities import ViewType
26
- from mlflow.tracking.fluent import SEARCH_MAX_RESULTS_PANDAS, _get_experiment_id
27
15
 
28
16
  from hydraflow.config import iter_params
29
17
  from hydraflow.run_collection import RunCollection
30
- from hydraflow.utils import get_artifact_dir
18
+ from hydraflow.utils import file_uri_to_path, get_artifact_dir
31
19
 
32
20
  if TYPE_CHECKING:
33
21
  from pathlib import Path
22
+ from typing import Any
34
23
 
35
- from mlflow.entities.experiment import Experiment
36
-
37
-
38
- def set_experiment(
39
- prefix: str = "",
40
- suffix: str = "",
41
- uri: str | Path | None = None,
42
- name: str | None = None,
43
- ) -> Experiment:
44
- """Set the experiment name and tracking URI optionally.
45
24
 
46
- This function sets the experiment name by combining the given prefix,
47
- the job name from HydraConfig, and the given suffix. Optionally, it can
48
- also set the tracking URI.
49
-
50
- Args:
51
- prefix (str): The prefix to prepend to the experiment name.
52
- suffix (str): The suffix to append to the experiment name.
53
- uri (str | Path | None): The tracking URI to use. Defaults to None.
54
- name (str | None): The name of the experiment. Defaults to None.
55
-
56
- Returns:
57
- Experiment: An instance of `mlflow.entities.Experiment` representing
58
- the new active experiment.
59
-
60
- """
61
- if uri is not None:
62
- mlflow.set_tracking_uri(uri)
63
-
64
- if name is not None:
65
- return mlflow.set_experiment(name)
66
-
67
- hc = HydraConfig.get()
68
- name = f"{prefix}{hc.job.name}{suffix}"
69
- return mlflow.set_experiment(name)
70
-
71
-
72
- def log_params(config: object, *, synchronous: bool | None = None) -> None:
25
+ def log_params(config: Any, *, synchronous: bool | None = None) -> None:
73
26
  """Log the parameters from the given configuration object.
74
27
 
75
28
  This method logs the parameters from the provided configuration object
@@ -77,7 +30,7 @@ def log_params(config: object, *, synchronous: bool | None = None) -> None:
77
30
  `mlflow.log_param` method.
78
31
 
79
32
  Args:
80
- config (object): The configuration object to log the parameters from.
33
+ config (Any): The configuration object to log the parameters from.
81
34
  synchronous (bool | None): Whether to log the parameters synchronously.
82
35
  Defaults to None.
83
36
 
@@ -86,71 +39,32 @@ def log_params(config: object, *, synchronous: bool | None = None) -> None:
86
39
  mlflow.log_param(key, value, synchronous=synchronous)
87
40
 
88
41
 
89
- def search_runs( # noqa: PLR0913
90
- *,
91
- experiment_ids: list[str] | None = None,
92
- filter_string: str = "",
93
- run_view_type: int = ViewType.ACTIVE_ONLY,
94
- max_results: int = SEARCH_MAX_RESULTS_PANDAS,
95
- order_by: list[str] | None = None,
96
- search_all_experiments: bool = False,
97
- experiment_names: list[str] | None = None,
98
- ) -> RunCollection:
99
- """Search for Runs that fit the specified criteria.
100
-
101
- This function wraps the `mlflow.search_runs` function and returns the
102
- results as a `RunCollection` object. It allows for flexible searching of
103
- MLflow runs based on various criteria.
42
+ def log_text(from_dir: Path, pattern: str = "*.log") -> None:
43
+ """Log text files in the given directory as artifacts.
104
44
 
105
- Note:
106
- The returned runs are sorted by their start time in ascending order.
45
+ Append the text files to the existing text file in the artifact directory.
107
46
 
108
47
  Args:
109
- experiment_ids (list[str] | None): List of experiment IDs. Search can
110
- work with experiment IDs or experiment names, but not both in the
111
- same call. Values other than ``None`` or ``[]`` will result in
112
- error if ``experiment_names`` is also not ``None`` or ``[]``.
113
- ``None`` will default to the active experiment if ``experiment_names``
114
- is ``None`` or ``[]``.
115
- filter_string (str): Filter query string, defaults to searching all
116
- runs.
117
- run_view_type (int): one of enum values ``ACTIVE_ONLY``, ``DELETED_ONLY``,
118
- or ``ALL`` runs defined in :py:class:`mlflow.entities.ViewType`.
119
- max_results (int): The maximum number of runs to put in the dataframe.
120
- Default is 100,000 to avoid causing out-of-memory issues on the user's
121
- machine.
122
- order_by (list[str] | None): List of columns to order by (e.g.,
123
- "metrics.rmse"). The ``order_by`` column can contain an optional
124
- ``DESC`` or ``ASC`` value. The default is ``ASC``. The default
125
- ordering is to sort by ``start_time DESC``, then ``run_id``.
126
- ``start_time DESC``, then ``run_id``.
127
- search_all_experiments (bool): Boolean specifying whether all
128
- experiments should be searched. Only honored if ``experiment_ids``
129
- is ``[]`` or ``None``.
130
- experiment_names (list[str] | None): List of experiment names. Search
131
- can work with experiment IDs or experiment names, but not both in
132
- the same call. Values other than ``None`` or ``[]`` will result in
133
- error if ``experiment_ids`` is also not ``None`` or ``[]``.
134
- ``experiment_ids`` is also not ``None`` or ``[]``. ``None`` will
135
- default to the active experiment if ``experiment_ids`` is ``None``
136
- or ``[]``.
137
-
138
- Returns:
139
- A `RunCollection` object containing the search results.
48
+ from_dir (Path): The directory to find the logs in.
49
+ pattern (str): The pattern to match the logs.
140
50
 
141
51
  """
142
- runs = mlflow.search_runs(
143
- experiment_ids=experiment_ids,
144
- filter_string=filter_string,
145
- run_view_type=run_view_type,
146
- max_results=max_results,
147
- order_by=order_by,
148
- output_format="list",
149
- search_all_experiments=search_all_experiments,
150
- experiment_names=experiment_names,
151
- )
152
- runs = sorted(runs, key=lambda run: run.info.start_time) # type: ignore
153
- return RunCollection(runs) # type: ignore
52
+ artifact_dir = get_artifact_dir()
53
+
54
+ for file in from_dir.glob(pattern):
55
+ if not file.is_file():
56
+ continue
57
+
58
+ file_artifact = artifact_dir / file.name
59
+ if file_artifact.exists():
60
+ text = file_artifact.read_text()
61
+ if not text.endswith("\n"):
62
+ text += "\n"
63
+ else:
64
+ text = ""
65
+
66
+ text += file.read_text()
67
+ mlflow.log_text(text, file.name)
154
68
 
155
69
 
156
70
  def list_run_paths(
@@ -160,20 +74,14 @@ def list_run_paths(
160
74
  """List all run paths for the specified experiments.
161
75
 
162
76
  This function retrieves all run paths for the given list of experiment names.
163
- If no experiment names are provided (None), it defaults to searching all runs
164
- for the currently active experiment. If an empty list is provided, the function
165
- will search all runs for all experiments except the "Default" experiment.
166
- The function returns the results as a list of `Path` objects.
167
-
168
- Note:
169
- The returned runs are sorted by their start time in ascending order.
77
+ If no experiment names are provided (None), the function will search all runs
78
+ for all experiments except the "Default" experiment.
170
79
 
171
80
  Args:
172
81
  experiment_names (list[str] | None): List of experiment names to search
173
- for runs. If None or an empty list is provided, the function will
174
- search the currently active experiment or all experiments except
175
- the "Default" experiment.
176
- other (str): The parts of the run directory to join.
82
+ for runs. If None is provided, the function will search all runs
83
+ for all experiments except the "Default" experiment.
84
+ *other (str): The parts of the run directory to join.
177
85
 
178
86
  Returns:
179
87
  list[Path]: A list of run paths for the specified experiments.
@@ -182,14 +90,10 @@ def list_run_paths(
182
90
  if isinstance(experiment_names, str):
183
91
  experiment_names = [experiment_names]
184
92
 
185
- elif experiment_names == []:
93
+ elif experiment_names is None:
186
94
  experiments = mlflow.search_experiments()
187
95
  experiment_names = [e.name for e in experiments if e.name != "Default"]
188
96
 
189
- if experiment_names is None:
190
- experiment_id = _get_experiment_id()
191
- experiment_names = [mlflow.get_experiment(experiment_id).name]
192
-
193
97
  run_paths: list[Path] = []
194
98
 
195
99
  for name in experiment_names:
@@ -197,7 +101,7 @@ def list_run_paths(
197
101
  uri = experiment.artifact_location
198
102
 
199
103
  if isinstance(uri, str):
200
- path = get_artifact_dir(uri=uri)
104
+ path = file_uri_to_path(uri)
201
105
  run_paths.extend(p for p in path.iterdir() if p.is_dir())
202
106
 
203
107
  if other:
@@ -210,38 +114,30 @@ def list_run_ids(experiment_names: str | list[str] | None = None) -> list[str]:
210
114
  """List all run IDs for the specified experiments.
211
115
 
212
116
  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.
117
+ If no experiment names are provided (None), the function will search all
118
+ runs for all experiments except the "Default" experiment.
220
119
 
221
120
  Args:
222
121
  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.
122
+ for runs. If None is provided, the function will search all runs
123
+ for all experiments except the "Default" experiment.
226
124
 
227
125
  Returns:
228
126
  list[str]: A list of run IDs for the specified experiments.
229
127
 
230
128
  """
231
- return [run_dir.stem for run_dir in list_run_paths(experiment_names)]
129
+ return [run_path.stem for run_path in list_run_paths(experiment_names)]
232
130
 
233
131
 
234
132
  def list_runs(
235
133
  experiment_names: str | list[str] | None = None,
236
134
  n_jobs: int = 0,
237
- status: str | list[str] | int | list[int] | None = None,
238
135
  ) -> RunCollection:
239
136
  """List all runs for the specified experiments.
240
137
 
241
138
  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.
139
+ If no experiment names are provided (None), the function will search all runs
140
+ for all experiments except the "Default" experiment.
245
141
  The function returns the results as a `RunCollection` object.
246
142
 
247
143
  Note:
@@ -249,13 +145,9 @@ def list_runs(
249
145
 
250
146
  Args:
251
147
  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.
148
+ for runs. If None is provided, the function will search all runs
149
+ for all experiments except the "Default" experiment.
150
+ n_jobs (int): The number of jobs to retrieve runs in parallel.
259
151
 
260
152
  Returns:
261
153
  RunCollection: A `RunCollection` instance containing the runs for the
@@ -269,12 +161,7 @@ def list_runs(
269
161
 
270
162
  else:
271
163
  it = (joblib.delayed(mlflow.get_run)(run_id) for run_id in run_ids)
272
- runs = joblib.Parallel(n_jobs, prefer="threads")(it)
164
+ runs = joblib.Parallel(n_jobs, backend="threading")(it)
273
165
 
274
166
  runs = sorted(runs, key=lambda run: run.info.start_time) # type: ignore
275
- rc = RunCollection(runs) # type: ignore
276
-
277
- if status is None:
278
- return rc
279
-
280
- return rc.filter(status=status)
167
+ return RunCollection(runs) # type: ignore
hydraflow/param.py CHANGED
@@ -18,7 +18,7 @@ if TYPE_CHECKING:
18
18
  from mlflow.entities import Run
19
19
 
20
20
 
21
- def match(param: str, value: Any) -> bool: # noqa: PLR0911
21
+ def match(param: str, value: Any) -> bool:
22
22
  """Check if the string matches the specified value.
23
23
 
24
24
  Args:
@@ -68,7 +68,7 @@ def _match_list(param: str, value: list) -> bool | None:
68
68
 
69
69
 
70
70
  def _match_tuple(param: str, value: tuple) -> bool | None:
71
- if len(value) != 2: # noqa: PLR2004
71
+ if len(value) != 2:
72
72
  return None
73
73
 
74
74
  if any(param.startswith(x) for x in ["[", "(", "{"]):
@@ -21,7 +21,7 @@ from __future__ import annotations
21
21
 
22
22
  from dataclasses import dataclass, field
23
23
  from itertools import chain
24
- from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, overload
24
+ from typing import TYPE_CHECKING, Any, overload
25
25
 
26
26
  from mlflow.entities import RunStatus
27
27
 
@@ -34,15 +34,9 @@ from hydraflow.utils import load_config
34
34
 
35
35
  if TYPE_CHECKING:
36
36
  from collections.abc import Callable, Iterator
37
- from pathlib import Path
38
37
  from typing import Any
39
38
 
40
39
  from mlflow.entities.run import Run
41
- from omegaconf import DictConfig
42
-
43
-
44
- T = TypeVar("T")
45
- P = ParamSpec("P")
46
40
 
47
41
 
48
42
  @dataclass
@@ -124,11 +118,6 @@ class RunCollection:
124
118
  runs = [run for run in self._runs if run not in other._runs] # noqa: SLF001
125
119
  return self.__class__(runs)
126
120
 
127
- @classmethod
128
- def from_list(cls, runs: list[Run]) -> RunCollection:
129
- """Create a `RunCollection` instance from a list of MLflow `Run` instances."""
130
- return cls(runs)
131
-
132
121
  @property
133
122
  def info(self) -> RunCollectionInfo:
134
123
  """An instance of `RunCollectionInfo`."""
@@ -139,26 +128,6 @@ class RunCollection:
139
128
  """An instance of `RunCollectionData`."""
140
129
  return self._data
141
130
 
142
- def take(self, n: int) -> RunCollection:
143
- """Take the first n runs from the collection.
144
-
145
- If n is negative, the method returns the last n runs
146
- from the collection.
147
-
148
- Args:
149
- n (int): The number of runs to take. If n is negative, the method
150
- returns the last n runs from the collection.
151
-
152
- Returns:
153
- A new `RunCollection` instance containing the first n runs if n is
154
- positive, or the last n runs if n is negative.
155
-
156
- """
157
- if n < 0:
158
- return self.__class__(self._runs[n:])
159
-
160
- return self.__class__(self._runs[:n])
161
-
162
131
  def one(self) -> Run:
163
132
  """Get the only `Run` instance in the collection.
164
133
 
@@ -238,8 +207,8 @@ class RunCollection:
238
207
  self,
239
208
  config: object | Callable[[Run], bool] | None = None,
240
209
  *,
241
- override: bool = False,
242
210
  select: list[str] | None = None,
211
+ overrides: list[str] | None = None,
243
212
  status: str | list[str] | int | list[int] | None = None,
244
213
  **kwargs,
245
214
  ) -> RunCollection:
@@ -264,9 +233,9 @@ class RunCollection:
264
233
  to filter the runs. This can be any object that provides key-value
265
234
  pairs through the `iter_params` function, or a callable that
266
235
  takes a `Run` object and returns a boolean value.
267
- override (bool): If True, override the configuration object with the
268
- provided key-value pairs.
269
236
  select (list[str] | None): The list of parameters to select.
237
+ overrides (list[str] | None): The list of overrides to filter the
238
+ runs.
270
239
  status (str | list[str] | int | list[int] | None): The status of the
271
240
  runs to filter.
272
241
  **kwargs: Additional key-value pairs to filter the runs.
@@ -279,8 +248,8 @@ class RunCollection:
279
248
  filter_runs(
280
249
  self._runs,
281
250
  config,
282
- override=override,
283
251
  select=select,
252
+ overrides=overrides,
284
253
  status=status,
285
254
  **kwargs,
286
255
  ),
@@ -400,121 +369,6 @@ class RunCollection:
400
369
 
401
370
  return params
402
371
 
403
- def map(
404
- self,
405
- func: Callable[Concatenate[Run, P], T],
406
- *args: P.args,
407
- **kwargs: P.kwargs,
408
- ) -> Iterator[T]:
409
- """Return an iterator of results by applying a function to each run.
410
-
411
- This method iterates over each run in the collection and applies the
412
- provided function to it, along with any additional arguments and
413
- keyword arguments.
414
-
415
- Args:
416
- func (Callable[[Run, P], T]): A function that takes a run and
417
- additional arguments and returns a result.
418
- *args: Additional arguments to pass to the function.
419
- **kwargs: Additional keyword arguments to pass to the function.
420
-
421
- Yields:
422
- Results obtained by applying the function to each run in the collection.
423
-
424
- """
425
- return (func(run, *args, **kwargs) for run in self)
426
-
427
- def map_id(
428
- self,
429
- func: Callable[Concatenate[str, P], T],
430
- *args: P.args,
431
- **kwargs: P.kwargs,
432
- ) -> Iterator[T]:
433
- """Return an iterator of results by applying a function to each run id.
434
-
435
- Args:
436
- func (Callable[[str, P], T]): A function that takes a run id and returns a
437
- result.
438
- *args: Additional arguments to pass to the function.
439
- **kwargs: Additional keyword arguments to pass to the function.
440
-
441
- Yields:
442
- Results obtained by applying the function to each run id in the
443
- collection.
444
-
445
- """
446
- return (func(run_id, *args, **kwargs) for run_id in self.info.run_id)
447
-
448
- def map_config(
449
- self,
450
- func: Callable[Concatenate[DictConfig, P], T],
451
- *args: P.args,
452
- **kwargs: P.kwargs,
453
- ) -> Iterator[T]:
454
- """Return an iterator of results by applying a function to each run config.
455
-
456
- Args:
457
- func (Callable[[DictConfig, P], T]): A function that takes a run
458
- configuration and returns a result.
459
- *args: Additional arguments to pass to the function.
460
- **kwargs: Additional keyword arguments to pass to the function.
461
-
462
- Yields:
463
- Results obtained by applying the function to each run configuration
464
- in the collection.
465
-
466
- """
467
- return (func(load_config(run), *args, **kwargs) for run in self)
468
-
469
- def map_uri(
470
- self,
471
- func: Callable[Concatenate[str | None, P], T],
472
- *args: P.args,
473
- **kwargs: P.kwargs,
474
- ) -> Iterator[T]:
475
- """Return an iterator of results by applying a function to each artifact URI.
476
-
477
- Iterate over each run in the collection, retrieves the artifact URI, and
478
- apply the provided function to it. If a run does not have an artifact
479
- URI, None is passed to the function.
480
-
481
- Args:
482
- func (Callable[[str | None, P], T]): A function that takes an
483
- artifact URI (string or None) and returns a result.
484
- *args: Additional arguments to pass to the function.
485
- **kwargs: Additional keyword arguments to pass to the function.
486
-
487
- Yields:
488
- Results obtained by applying the function to each artifact URI in the
489
- collection.
490
-
491
- """
492
- return (func(uri, *args, **kwargs) for uri in self.info.artifact_uri)
493
-
494
- def map_dir(
495
- self,
496
- func: Callable[Concatenate[Path, P], T],
497
- *args: P.args,
498
- **kwargs: P.kwargs,
499
- ) -> Iterator[T]:
500
- """Return an iterator of results by applying a function to each artifact dir.
501
-
502
- Iterate over each run in the collection, downloads the artifact
503
- directory, and apply the provided function to the directory path.
504
-
505
- Args:
506
- func (Callable[[Path, P], T]): A function that takes an artifact directory
507
- path (string) and returns a result.
508
- *args: Additional arguments to pass to the function.
509
- **kwargs: Additional keyword arguments to pass to the function.
510
-
511
- Yields:
512
- Results obtained by applying the function to each artifact directory
513
- in the collection.
514
-
515
- """
516
- return (func(dir, *args, **kwargs) for dir in self.info.artifact_dir) # noqa: A001
517
-
518
372
  def groupby(
519
373
  self,
520
374
  names: str | list[str],
@@ -631,8 +485,8 @@ def filter_runs(
631
485
  runs: list[Run],
632
486
  config: object | Callable[[Run], bool] | None = None,
633
487
  *,
634
- override: bool = False,
635
488
  select: list[str] | None = None,
489
+ overrides: list[str] | None = None,
636
490
  status: str | list[str] | int | list[int] | None = None,
637
491
  **kwargs,
638
492
  ) -> list[Run]:
@@ -658,10 +512,10 @@ def filter_runs(
658
512
  that provides key-value pairs through the `iter_params` function.
659
513
  This can also be a callable that takes a `Run` object and returns
660
514
  a boolean value. Defaults to None.
661
- override (bool, optional): If True, filter the runs based on
662
- the overrides. Defaults to False.
663
515
  select (list[str] | None, optional): The list of parameters to select.
664
516
  Defaults to None.
517
+ overrides (list[str] | None, optional): The list of overrides to filter the
518
+ runs. Defaults to None.
665
519
  status (str | list[str] | RunStatus | list[RunStatus] | None, optional): The
666
520
  status of the runs to filter. Defaults to None.
667
521
  **kwargs: Additional key-value pairs to filter the runs.
@@ -674,8 +528,8 @@ def filter_runs(
674
528
  runs = [run for run in runs if config(run)]
675
529
 
676
530
  else:
677
- if override:
678
- config = select_overrides(config)
531
+ if overrides:
532
+ config = select_overrides(config, overrides)
679
533
  elif select:
680
534
  config = select_config(config, select)
681
535
 
hydraflow/run_data.py CHANGED
@@ -6,7 +6,8 @@ from typing import TYPE_CHECKING
6
6
 
7
7
  from pandas import DataFrame
8
8
 
9
- from hydraflow.config import collect_params
9
+ from hydraflow.config import iter_params
10
+ from hydraflow.utils import load_config
10
11
 
11
12
  if TYPE_CHECKING:
12
13
  from collections.abc import Iterable
@@ -39,7 +40,8 @@ class RunCollectionData:
39
40
  A DataFrame containing the runs' configurations.
40
41
 
41
42
  """
42
- return DataFrame(self._runs.map_config(collect_params))
43
+ values = [dict(iter_params(load_config(r))) for r in self._runs]
44
+ return DataFrame(values)
43
45
 
44
46
 
45
47
  def _to_dict(it: Iterable[dict[str, Any]]) -> dict[str, list[Any]]:
hydraflow/utils.py CHANGED
@@ -12,46 +12,42 @@ import mlflow
12
12
  import mlflow.artifacts
13
13
  from hydra.core.hydra_config import HydraConfig
14
14
  from mlflow.entities import Run
15
- from omegaconf import DictConfig, OmegaConf
15
+ from omegaconf import DictConfig, ListConfig, OmegaConf
16
16
 
17
17
  if TYPE_CHECKING:
18
18
  from collections.abc import Iterable
19
19
 
20
20
 
21
- def get_artifact_dir(run: Run | None = None, uri: str | None = None) -> Path:
21
+ def file_uri_to_path(uri: str) -> Path:
22
+ """Convert a file URI to a local path."""
23
+ if not uri.startswith("file:"):
24
+ return Path(uri)
25
+
26
+ path = urllib.parse.urlparse(uri).path
27
+ return Path(urllib.request.url2pathname(path)) # for Windows
28
+
29
+
30
+ def get_artifact_dir(run: Run | None = None) -> Path:
22
31
  """Retrieve the artifact directory for the given run.
23
32
 
24
33
  This function uses MLflow to get the artifact directory for the given run.
25
34
 
26
35
  Args:
27
36
  run (Run | None): The run object. Defaults to None.
28
- uri (str | None): The URI of the artifact. Defaults to None.
29
37
 
30
38
  Returns:
31
39
  The local path to the directory where the artifacts are downloaded.
32
40
 
33
41
  """
34
- if run is not None and uri is not None:
35
- raise ValueError("Cannot provide both run and uri")
36
-
37
- if run is None and uri is None:
42
+ if run is None:
38
43
  uri = mlflow.get_artifact_uri()
39
- elif run:
44
+ else:
40
45
  uri = run.info.artifact_uri
41
46
 
42
47
  if not isinstance(uri, str):
43
48
  raise NotImplementedError
44
49
 
45
- if uri.startswith("file:"):
46
- return file_uri_to_path(uri)
47
-
48
- return Path(uri)
49
-
50
-
51
- def file_uri_to_path(uri: str) -> Path:
52
- """Convert a file URI to a local path."""
53
- path = urllib.parse.urlparse(uri).path
54
- return Path(urllib.request.url2pathname(path)) # for Windows
50
+ return file_uri_to_path(uri)
55
51
 
56
52
 
57
53
  def get_artifact_path(run: Run | None, path: str) -> Path:
@@ -123,12 +119,7 @@ def load_config(run: Run) -> DictConfig:
123
119
  return OmegaConf.load(path) # type: ignore
124
120
 
125
121
 
126
- def get_overrides() -> list[str]:
127
- """Retrieve the overrides for the current run."""
128
- return list(HydraConfig.get().overrides.task) # ListConifg -> list
129
-
130
-
131
- def load_overrides(run: Run) -> list[str]:
122
+ def load_overrides(run: Run) -> ListConfig:
132
123
  """Load the overrides for a given run.
133
124
 
134
125
  This function loads the overrides for the provided Run instance
@@ -137,15 +128,15 @@ def load_overrides(run: Run) -> list[str]:
137
128
  `.hydra/overrides.yaml` is not found in the run's artifact directory.
138
129
 
139
130
  Args:
140
- run (Run): The Run instance for which to load the overrides.
131
+ run (Run): The Run instance for which to load the configuration.
141
132
 
142
133
  Returns:
143
- The loaded overrides as a list of strings. Returns an empty list
144
- if the overrides file is not found.
134
+ The loaded configuration as a DictConfig object. Returns an empty
135
+ DictConfig if the configuration file is not found.
145
136
 
146
137
  """
147
138
  path = get_artifact_dir(run) / ".hydra/overrides.yaml"
148
- return [str(x) for x in OmegaConf.load(path)]
139
+ return OmegaConf.load(path) # type: ignore
149
140
 
150
141
 
151
142
  def remove_run(run: Run | Iterable[Run]) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hydraflow
3
- Version: 0.7.5
3
+ Version: 0.8.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
@@ -108,7 +108,7 @@ class MySQLConfig:
108
108
  cs = ConfigStore.instance()
109
109
  cs.store(name="config", node=MySQLConfig)
110
110
 
111
- @hydra.main(version_base=None, config_name="config")
111
+ @hydra.main(config_name="config", version_base=None)
112
112
  def my_app(cfg: MySQLConfig) -> None:
113
113
  # Set experiment by Hydra job name.
114
114
  hydraflow.set_experiment()
@@ -0,0 +1,17 @@
1
+ hydraflow/__init__.py,sha256=yp4LT1FDYPIduR6PqJNuSm9kztVCpL1P0zcPHWGvaJU,712
2
+ hydraflow/cli.py,sha256=jxqFppNeJWAr2Tb-C_MQXEJtegJ6TXcd3C1CT7Jdb1A,1559
3
+ hydraflow/config.py,sha256=SJzjgsO_kzB78_whJ3lmy7GlZvTvwZONH1BJBn8zCuI,3817
4
+ hydraflow/context.py,sha256=H5xeNbhMS23U-epsucprl5G3lbOR1aO9nDES4QGLWNk,4747
5
+ hydraflow/main.py,sha256=O5ETCMCg12zXoaYlZMHcM4IYAs6GVTkADrmEssrtjkk,4994
6
+ hydraflow/mlflow.py,sha256=pRRsBaBBH4cfzSko-8mmo5bV04GGklxoO0kORkInypM,5663
7
+ hydraflow/param.py,sha256=LHU9j9_7oA99igasoOyKofKClVr9FmGA3UABJ-KmyS0,4538
8
+ hydraflow/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ hydraflow/run_collection.py,sha256=rtH1cglSlK3QFg9hhifo9lzjDa9veHpoyYxEOmIEM84,19646
10
+ hydraflow/run_data.py,sha256=S2NNFtA1TleqpgeK4mIn1YY8YbWJFyhF7wXR5NWeYLk,1604
11
+ hydraflow/run_info.py,sha256=Jf5wrIjRLIV1-k-obHDqwKHa6j_ZonrY8od-rXlbtMo,1024
12
+ hydraflow/utils.py,sha256=T4ESiepEcqR-FZlo_m7VTBEFMwalrqPI8eFKPagvv3Q,4402
13
+ hydraflow-0.8.0.dist-info/METADATA,sha256=J1ilgG7L4A8OvzgZSNycp0YgyHk5e8_gwTr9NN82Ejk,4767
14
+ hydraflow-0.8.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
+ hydraflow-0.8.0.dist-info/entry_points.txt,sha256=XI0khPbpCIUo9UPqkNEpgh-kqK3Jy8T7L2VCWOdkbSM,48
16
+ hydraflow-0.8.0.dist-info/licenses/LICENSE,sha256=IGdDrBPqz1O0v_UwCW-NJlbX9Hy9b3uJ11t28y2srmY,1062
17
+ hydraflow-0.8.0.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- hydraflow/__init__.py,sha256=0HJOiiKhfH3MFbuoL_BLaBaruVSb53Scimt2_2rRI28,995
2
- hydraflow/cli.py,sha256=jxqFppNeJWAr2Tb-C_MQXEJtegJ6TXcd3C1CT7Jdb1A,1559
3
- hydraflow/config.py,sha256=MNX9da5bPVDcjnpji7Cm9ndK6ura92pt361m4PRh6_E,4326
4
- hydraflow/context.py,sha256=3xfKhMozkKFqtWeOp9Gie0A5o5URMta4US6iVD5TcLU,6002
5
- hydraflow/main.py,sha256=hroncI_SNpNgEtdxLgzI397J5S2Amv7J0atnPxwBePM,1314
6
- hydraflow/mlflow.py,sha256=lKpY5tPJRXXlvT5ZFVz1kROHsuvzGhp5kp8RiT2jlX8,10912
7
- hydraflow/param.py,sha256=yu1aMNXRLegXGDL-68vwIkfeDF9CaU784WZENGLwl7Q,4572
8
- hydraflow/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- hydraflow/run_collection.py,sha256=YCWg5Dz1j49xB2LA75onq5wsAeQQbifXpG4yPUwRN4I,24776
10
- hydraflow/run_data.py,sha256=dpyyfnuH9mCtIZeigMo1iFQo9bafMdEL4i4uI2l0UqY,1525
11
- hydraflow/run_info.py,sha256=Jf5wrIjRLIV1-k-obHDqwKHa6j_ZonrY8od-rXlbtMo,1024
12
- hydraflow/utils.py,sha256=a9i5PEJn8Ssowv9dqHadAihZXlsqtVjHZ9MZvkPq1bY,4747
13
- hydraflow-0.7.5.dist-info/METADATA,sha256=oSBWEevJs2RI55hqrxzW3k9ArtwRrvnk1kBl7oJNohg,4767
14
- hydraflow-0.7.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
- hydraflow-0.7.5.dist-info/entry_points.txt,sha256=XI0khPbpCIUo9UPqkNEpgh-kqK3Jy8T7L2VCWOdkbSM,48
16
- hydraflow-0.7.5.dist-info/licenses/LICENSE,sha256=IGdDrBPqz1O0v_UwCW-NJlbX9Hy9b3uJ11t28y2srmY,1062
17
- hydraflow-0.7.5.dist-info/RECORD,,