hydraflow 0.2.16__py3-none-any.whl → 0.2.18__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
hydraflow/asyncio.py CHANGED
@@ -1,3 +1,5 @@
1
+ """Provide functionality for running commands and monitoring file changes."""
2
+
1
3
  from __future__ import annotations
2
4
 
3
5
  import asyncio
@@ -27,8 +29,7 @@ async def execute_command(
27
29
  stderr: Callable[[str], None] | None = None,
28
30
  stop_event: asyncio.Event,
29
31
  ) -> int:
30
- """
31
- Runs a command asynchronously and pass the output to callback functions.
32
+ """Run a command asynchronously and pass the output to callback functions.
32
33
 
33
34
  Args:
34
35
  program (str): The program to run.
@@ -39,6 +40,7 @@ async def execute_command(
39
40
 
40
41
  Returns:
41
42
  int: The return code of the process.
43
+
42
44
  """
43
45
  try:
44
46
  process = await asyncio.create_subprocess_exec(
@@ -68,13 +70,13 @@ async def process_stream(
68
70
  stream: StreamReader | None,
69
71
  callback: Callable[[str], None] | None,
70
72
  ) -> None:
71
- """
72
- Reads a stream asynchronously and pass each line to a callback function.
73
+ """Read a stream asynchronously and pass each line to a callback function.
73
74
 
74
75
  Args:
75
76
  stream (StreamReader | None): The stream to read from.
76
77
  callback (Callable[[str], None] | None): The callback function to handle
77
78
  each line.
79
+
78
80
  """
79
81
  if stream is None or callback is None:
80
82
  return
@@ -93,9 +95,7 @@ async def monitor_file_changes(
93
95
  stop_event: asyncio.Event,
94
96
  **awatch_kwargs,
95
97
  ) -> None:
96
- """
97
- Watches for file changes in specified paths and pass the changes to a
98
- callback function.
98
+ """Watch file changes in specified paths and pass the changes to a callback.
99
99
 
100
100
  Args:
101
101
  paths (list[str | Path]): List of paths to monitor for changes.
@@ -103,6 +103,7 @@ async def monitor_file_changes(
103
103
  function to handle file changes.
104
104
  stop_event (asyncio.Event): Event to signal when to stop watching.
105
105
  **awatch_kwargs: Additional keyword arguments to pass to watchfiles.awatch.
106
+
106
107
  """
107
108
  str_paths = [str(path) for path in paths]
108
109
  try:
@@ -127,8 +128,7 @@ async def run_and_monitor(
127
128
  paths: list[str | Path] | None = None,
128
129
  **awatch_kwargs,
129
130
  ) -> int:
130
- """
131
- Runs a command and optionally watch for file changes concurrently.
131
+ """Run a command and optionally watch for file changes concurrently.
132
132
 
133
133
  Args:
134
134
  program (str): The program to run.
@@ -138,6 +138,8 @@ async def run_and_monitor(
138
138
  watch (Callable[[set[tuple[Change, str]]], None] | None): Callback for
139
139
  file changes.
140
140
  paths (list[str | Path] | None): List of paths to monitor for changes.
141
+ **awatch_kwargs: Additional keyword arguments to pass to `watchfiles.awatch`.
142
+
141
143
  """
142
144
  stop_event = asyncio.Event()
143
145
  run_task = asyncio.create_task(
@@ -184,8 +186,7 @@ def run(
184
186
  paths: list[str | Path] | None = None,
185
187
  **awatch_kwargs,
186
188
  ) -> int:
187
- """
188
- Run a command synchronously and optionally watch for file changes.
189
+ """Run a command synchronously and optionally watch for file changes.
189
190
 
190
191
  This function is a synchronous wrapper around the asynchronous
191
192
  `run_and_monitor` function. It runs a specified command and optionally
@@ -208,6 +209,7 @@ def run(
208
209
 
209
210
  Returns:
210
211
  int: The return code of the process.
212
+
211
213
  """
212
214
  if watch and not paths:
213
215
  paths = [Path.cwd()]
hydraflow/config.py CHANGED
@@ -1,7 +1,4 @@
1
- """
2
- This module provides functionality for working with configuration
3
- objects using the OmegaConf library.
4
- """
1
+ """Provide functionality for working with configuration objects using the OmegaConf."""
5
2
 
6
3
  from __future__ import annotations
7
4
 
@@ -15,8 +12,7 @@ if TYPE_CHECKING:
15
12
 
16
13
 
17
14
  def iter_params(config: object, prefix: str = "") -> Iterator[tuple[str, Any]]:
18
- """
19
- Recursively iterate over the parameters in the given configuration object.
15
+ """Recursively iterate over the parameters in the given configuration object.
20
16
 
21
17
  This function traverses the configuration object and yields key-value pairs
22
18
  representing the parameters. The keys are prefixed with the provided prefix.
@@ -29,6 +25,7 @@ def iter_params(config: object, prefix: str = "") -> Iterator[tuple[str, Any]]:
29
25
 
30
26
  Yields:
31
27
  Key-value pairs representing the parameters in the configuration object.
28
+
32
29
  """
33
30
  if config is None:
34
31
  return
hydraflow/context.py CHANGED
@@ -1,7 +1,4 @@
1
- """
2
- This module provides context managers to log parameters and manage the MLflow
3
- run context.
4
- """
1
+ """Provide context managers to log parameters and manage the MLflow run context."""
5
2
 
6
3
  from __future__ import annotations
7
4
 
@@ -34,9 +31,7 @@ def log_run(
34
31
  *,
35
32
  synchronous: bool | None = None,
36
33
  ) -> Iterator[None]:
37
- """
38
- Log the parameters from the given configuration object and manage the MLflow
39
- run context.
34
+ """Log the parameters from the given configuration object.
40
35
 
41
36
  This context manager logs the parameters from the provided configuration object
42
37
  using MLflow. It also manages the MLflow run context, ensuring that artifacts
@@ -56,6 +51,7 @@ def log_run(
56
51
  # Perform operations within the MLflow run context
57
52
  pass
58
53
  ```
54
+
59
55
  """
60
56
  log_params(config, synchronous=synchronous)
61
57
 
@@ -98,8 +94,7 @@ def start_run( # noqa: PLR0913
98
94
  log_system_metrics: bool | None = None,
99
95
  synchronous: bool | None = None,
100
96
  ) -> Iterator[Run]:
101
- """
102
- Start an MLflow run and log parameters using the provided configuration object.
97
+ """Start an MLflow run and log parameters using the provided configuration object.
103
98
 
104
99
  This context manager starts an MLflow run and logs parameters using the specified
105
100
  configuration object. It ensures that the run is properly closed after completion.
@@ -130,6 +125,7 @@ def start_run( # noqa: PLR0913
130
125
  - `mlflow.start_run`: The MLflow function to start a run directly.
131
126
  - `log_run`: A context manager to log parameters and manage the MLflow
132
127
  run context.
128
+
133
129
  """
134
130
  with (
135
131
  mlflow.start_run(
@@ -156,9 +152,7 @@ def watch(
156
152
  ignore_patterns: list[str] | None = None,
157
153
  ignore_log: bool = True,
158
154
  ) -> Iterator[None]:
159
- """
160
- Watch the given directory for changes and call the provided function
161
- when a change is detected.
155
+ """Watch the given directory for changes.
162
156
 
163
157
  This context manager sets up a file system watcher on the specified directory.
164
158
  When a file modification is detected, the provided function is called with
@@ -173,6 +167,9 @@ def watch(
173
167
  the current MLflow artifact URI is used. Defaults to "".
174
168
  timeout (int): The timeout period in seconds for the watcher
175
169
  to run after the context is exited. Defaults to 60.
170
+ ignore_patterns (list[str] | None): A list of glob patterns to ignore.
171
+ Defaults to None.
172
+ ignore_log (bool): Whether to ignore log files. Defaults to True.
176
173
 
177
174
  Yields:
178
175
  None
@@ -183,6 +180,7 @@ def watch(
183
180
  # Perform operations while watching the directory for changes
184
181
  pass
185
182
  ```
183
+
186
184
  """
187
185
  dir = dir or get_artifact_dir() # noqa: A001
188
186
  if isinstance(dir, Path):
@@ -214,6 +212,8 @@ def watch(
214
212
 
215
213
 
216
214
  class Handler(PatternMatchingEventHandler):
215
+ """Monitor file changes and call the given function when a change is detected."""
216
+
217
217
  def __init__(
218
218
  self,
219
219
  func: Callable[[Path], None],
@@ -232,6 +232,7 @@ class Handler(PatternMatchingEventHandler):
232
232
  super().__init__(ignore_patterns=ignore_patterns)
233
233
 
234
234
  def on_modified(self, event: FileModifiedEvent) -> None:
235
+ """Modify when a file is modified."""
235
236
  file = Path(str(event.src_path))
236
237
  if file.is_file():
237
238
  self.func(file)
@@ -242,9 +243,7 @@ def chdir_artifact(
242
243
  run: Run,
243
244
  artifact_path: str | None = None,
244
245
  ) -> Iterator[Path]:
245
- """
246
- Change the current working directory to the artifact directory of the
247
- given run.
246
+ """Change the current working directory to the artifact directory of the given run.
248
247
 
249
248
  This context manager changes the current working directory to the artifact
250
249
  directory of the given run. It ensures that the directory is changed back
@@ -253,6 +252,7 @@ def chdir_artifact(
253
252
  Args:
254
253
  run (Run): The run to get the artifact directory from.
255
254
  artifact_path (str | None): The artifact path.
255
+
256
256
  """
257
257
  curdir = Path.cwd()
258
258
  path = mlflow.artifacts.download_artifacts(
hydraflow/info.py CHANGED
@@ -1,3 +1,5 @@
1
+ """Provide information about MLflow runs."""
2
+
1
3
  from __future__ import annotations
2
4
 
3
5
  from pathlib import Path
@@ -15,37 +17,44 @@ if TYPE_CHECKING:
15
17
 
16
18
 
17
19
  class RunCollectionInfo:
20
+ """Provide information about MLflow runs."""
21
+
18
22
  def __init__(self, runs: RunCollection) -> None:
19
23
  self._runs = runs
20
24
 
21
25
  @property
22
26
  def run_id(self) -> list[str]:
27
+ """Get the run ID for each run in the collection."""
23
28
  return [run.info.run_id for run in self._runs]
24
29
 
25
30
  @property
26
31
  def params(self) -> list[dict[str, str]]:
32
+ """Get the parameters for each run in the collection."""
27
33
  return [run.data.params for run in self._runs]
28
34
 
29
35
  @property
30
36
  def metrics(self) -> list[dict[str, float]]:
37
+ """Get the metrics for each run in the collection."""
31
38
  return [run.data.metrics for run in self._runs]
32
39
 
33
40
  @property
34
41
  def artifact_uri(self) -> list[str | None]:
42
+ """Get the artifact URI for each run in the collection."""
35
43
  return [run.info.artifact_uri for run in self._runs]
36
44
 
37
45
  @property
38
46
  def artifact_dir(self) -> list[Path]:
47
+ """Get the artifact directory for each run in the collection."""
39
48
  return [get_artifact_dir(run) for run in self._runs]
40
49
 
41
50
  @property
42
51
  def config(self) -> list[DictConfig]:
52
+ """Get the configuration for each run in the collection."""
43
53
  return [load_config(run) for run in self._runs]
44
54
 
45
55
 
46
56
  def get_artifact_dir(run: Run | None = None) -> Path:
47
- """
48
- Retrieve the artifact directory for the given run.
57
+ """Retrieve the artifact directory for the given run.
49
58
 
50
59
  This function uses MLflow to get the artifact directory for the given run.
51
60
 
@@ -54,6 +63,7 @@ def get_artifact_dir(run: Run | None = None) -> Path:
54
63
 
55
64
  Returns:
56
65
  The local path to the directory where the artifacts are downloaded.
66
+
57
67
  """
58
68
  if run is None:
59
69
  uri = mlflow.get_artifact_uri()
@@ -64,8 +74,7 @@ def get_artifact_dir(run: Run | None = None) -> Path:
64
74
 
65
75
 
66
76
  def get_hydra_output_dir(run: Run | None = None) -> Path:
67
- """
68
- Retrieve the Hydra output directory for the given run.
77
+ """Retrieve the Hydra output directory for the given run.
69
78
 
70
79
  This function returns the Hydra output directory. If no run is provided,
71
80
  it retrieves the output directory from the current Hydra configuration.
@@ -82,6 +91,7 @@ def get_hydra_output_dir(run: Run | None = None) -> Path:
82
91
  Raises:
83
92
  FileNotFoundError: If the Hydra configuration file is not found
84
93
  in the artifacts.
94
+
85
95
  """
86
96
  if run is None:
87
97
  hc = HydraConfig.get()
@@ -97,8 +107,7 @@ def get_hydra_output_dir(run: Run | None = None) -> Path:
97
107
 
98
108
 
99
109
  def load_config(run: Run) -> DictConfig:
100
- """
101
- Load the configuration for a given run.
110
+ """Load the configuration for a given run.
102
111
 
103
112
  This function loads the configuration for the provided Run instance
104
113
  by downloading the configuration file from the MLflow artifacts and
@@ -111,6 +120,7 @@ def load_config(run: Run) -> DictConfig:
111
120
  Returns:
112
121
  The loaded configuration as a DictConfig object. Returns an empty
113
122
  DictConfig if the configuration file is not found.
123
+
114
124
  """
115
125
  path = get_artifact_dir(run) / ".hydra/config.yaml"
116
126
  return OmegaConf.load(path) # type: ignore
hydraflow/mlflow.py CHANGED
@@ -1,20 +1,17 @@
1
- """
2
- This module provides functionality to log parameters from Hydra configuration objects
3
- and set up experiments using MLflow. It includes methods for managing experiments,
4
- searching for runs, and logging parameters and artifacts.
1
+ """Provide functionality to log parameters from Hydra configuration objects.
2
+
3
+ This module provides functions to log parameters from Hydra configuration objects
4
+ to MLflow, set experiments, and manage tracking URIs. It integrates Hydra's
5
+ configuration management with MLflow's experiment tracking capabilities.
5
6
 
6
7
  Key Features:
7
- - **Experiment Management**: Set and manage MLflow experiments with customizable names
8
- based on Hydra configuration.
9
- - **Run Logging**: Log parameters and metrics from Hydra configuration objects to
10
- MLflow, ensuring that all relevant information is captured during experiments.
11
- - **Run Search**: Search for runs based on various criteria, allowing for flexible
12
- retrieval of experiment results.
13
- - **Artifact Management**: Retrieve and log artifacts associated with runs, facilitating
14
- easy access to outputs generated during experiments.
15
-
16
- This module is designed to integrate seamlessly with Hydra, providing a robust
17
- solution for tracking machine learning experiments and their associated metadata.
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.
18
15
  """
19
16
 
20
17
  from __future__ import annotations
@@ -40,8 +37,7 @@ def set_experiment(
40
37
  suffix: str = "",
41
38
  uri: str | Path | None = None,
42
39
  ) -> Experiment:
43
- """
44
- Sets the experiment name and tracking URI optionally.
40
+ """Set the experiment name and tracking URI optionally.
45
41
 
46
42
  This function sets the experiment name by combining the given prefix,
47
43
  the job name from HydraConfig, and the given suffix. Optionally, it can
@@ -55,6 +51,7 @@ def set_experiment(
55
51
  Returns:
56
52
  Experiment: An instance of `mlflow.entities.Experiment` representing
57
53
  the new active experiment.
54
+
58
55
  """
59
56
  if uri is not None:
60
57
  mlflow.set_tracking_uri(uri)
@@ -65,8 +62,7 @@ def set_experiment(
65
62
 
66
63
 
67
64
  def log_params(config: object, *, synchronous: bool | None = None) -> None:
68
- """
69
- Log the parameters from the given configuration object.
65
+ """Log the parameters from the given configuration object.
70
66
 
71
67
  This method logs the parameters from the provided configuration object
72
68
  using MLflow. It iterates over the parameters and logs them using the
@@ -76,6 +72,7 @@ def log_params(config: object, *, synchronous: bool | None = None) -> None:
76
72
  config (object): The configuration object to log the parameters from.
77
73
  synchronous (bool | None): Whether to log the parameters synchronously.
78
74
  Defaults to None.
75
+
79
76
  """
80
77
  for key, value in iter_params(config):
81
78
  mlflow.log_param(key, value, synchronous=synchronous)
@@ -91,8 +88,7 @@ def search_runs( # noqa: PLR0913
91
88
  search_all_experiments: bool = False,
92
89
  experiment_names: list[str] | None = None,
93
90
  ) -> RunCollection:
94
- """
95
- Search for Runs that fit the specified criteria.
91
+ """Search for Runs that fit the specified criteria.
96
92
 
97
93
  This function wraps the `mlflow.search_runs` function and returns the
98
94
  results as a `RunCollection` object. It allows for flexible searching of
@@ -133,6 +129,7 @@ def search_runs( # noqa: PLR0913
133
129
 
134
130
  Returns:
135
131
  A `RunCollection` object containing the search results.
132
+
136
133
  """
137
134
  runs = mlflow.search_runs(
138
135
  experiment_ids=experiment_ids,
@@ -151,9 +148,9 @@ def search_runs( # noqa: PLR0913
151
148
  def list_runs(
152
149
  experiment_names: str | list[str] | None = None,
153
150
  n_jobs: int = 0,
151
+ status: str | list[str] | int | list[int] | None = None,
154
152
  ) -> RunCollection:
155
- """
156
- List all runs for the specified experiments.
153
+ """List all runs for the specified experiments.
157
154
 
158
155
  This function retrieves all runs for the given list of experiment names.
159
156
  If no experiment names are provided (None), it defaults to searching all runs
@@ -169,11 +166,27 @@ def list_runs(
169
166
  for runs. If None or an empty list is provided, the function will
170
167
  search the currently active experiment or all experiments except
171
168
  the "Default" experiment.
169
+ n_jobs (int): The number of jobs to run in parallel. If 0, the function
170
+ will search runs sequentially.
171
+ status (str | list[str] | int | list[int] | None): The status of the runs
172
+ to filter.
172
173
 
173
174
  Returns:
174
175
  RunCollection: A `RunCollection` instance containing the runs for the
175
176
  specified experiments.
177
+
176
178
  """
179
+ rc = _list_runs(experiment_names, n_jobs)
180
+ if status is None:
181
+ return rc
182
+
183
+ return rc.filter(status=status)
184
+
185
+
186
+ def _list_runs(
187
+ experiment_names: str | list[str] | None = None,
188
+ n_jobs: int = 0,
189
+ ) -> RunCollection:
177
190
  if isinstance(experiment_names, str):
178
191
  experiment_names = [experiment_names]
179
192
 
hydraflow/param.py ADDED
@@ -0,0 +1,75 @@
1
+ """Provide utility functions for parameter matching.
2
+
3
+ The main function `match` checks if a given parameter matches a specified value.
4
+ It supports various types of values including None, boolean, list, tuple, int,
5
+ float, and str.
6
+
7
+ Helper functions `_match_list` and `_match_tuple` are used internally to handle
8
+ matching for list and tuple types respectively.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any
14
+
15
+
16
+ def match(param: str, value: Any) -> bool:
17
+ """Check if the string matches the specified value.
18
+
19
+ Args:
20
+ param (str): The parameter to check.
21
+ value (Any): The value to check.
22
+
23
+ Returns:
24
+ True if the parameter matches the specified value,
25
+ False otherwise.
26
+
27
+ """
28
+ if value in [None, True, False]:
29
+ return param == str(value)
30
+
31
+ if isinstance(value, list) and (m := _match_list(param, value)) is not None:
32
+ return m
33
+
34
+ if isinstance(value, tuple) and (m := _match_tuple(param, value)) is not None:
35
+ return m
36
+
37
+ if isinstance(value, int | float | str):
38
+ return type(value)(param) == value
39
+
40
+ return param == str(value)
41
+
42
+
43
+ def _match_list(param: str, value: list) -> bool | None:
44
+ if not value:
45
+ return None
46
+
47
+ if any(param.startswith(x) for x in ["[", "(", "{"]):
48
+ return None
49
+
50
+ if isinstance(value[0], bool):
51
+ return None
52
+
53
+ if not isinstance(value[0], int | float | str):
54
+ return None
55
+
56
+ return type(value[0])(param) in value
57
+
58
+
59
+ def _match_tuple(param: str, value: tuple) -> bool | None:
60
+ if len(value) != 2: # noqa: PLR2004
61
+ return None
62
+
63
+ if any(param.startswith(x) for x in ["[", "(", "{"]):
64
+ return None
65
+
66
+ if isinstance(value[0], bool):
67
+ return None
68
+
69
+ if not isinstance(value[0], int | float | str):
70
+ return None
71
+
72
+ if type(value[0]) is not type(value[1]):
73
+ return None
74
+
75
+ return value[0] <= type(value[0])(param) < value[1] # type: ignore
hydraflow/progress.py CHANGED
@@ -1,18 +1,7 @@
1
- """
2
- Module for managing progress tracking in parallel processing using Joblib
3
- and Rich's Progress bar.
1
+ """Context managers and functions for parallel task execution with progress.
4
2
 
5
3
  Provide context managers and functions to facilitate the execution
6
4
  of tasks in parallel while displaying progress updates.
7
-
8
- The following key components are provided:
9
-
10
- - JoblibProgress: A context manager for tracking progress with Rich's progress
11
- bar.
12
- - parallel_progress: A function to execute a given function in parallel over
13
- an iterable with progress tracking.
14
- - multi_tasks_progress: A function to render auto-updating progress bars for
15
- multiple tasks concurrently.
16
5
  """
17
6
 
18
7
  from __future__ import annotations
@@ -37,8 +26,7 @@ def JoblibProgress( # noqa: N802
37
26
  total: int | None = None,
38
27
  **kwargs,
39
28
  ) -> Iterator[Progress]:
40
- """
41
- Context manager for tracking progress using Joblib with Rich's Progress bar.
29
+ """Context manager for tracking progress using Joblib with Rich's Progress bar.
42
30
 
43
31
  Args:
44
32
  *columns (ProgressColumn | str): Columns to display in the progress bar.
@@ -56,6 +44,7 @@ def JoblibProgress( # noqa: N802
56
44
  with JoblibProgress("task", total=100) as progress:
57
45
  # Your parallel processing code here
58
46
  ```
47
+
59
48
  """
60
49
  if not columns:
61
50
  columns = Progress.get_default_columns()
@@ -94,8 +83,7 @@ def parallel_progress(
94
83
  description: str | None = None,
95
84
  **kwargs,
96
85
  ) -> list[U]:
97
- """
98
- Execute a function in parallel over an iterable with progress tracking.
86
+ """Execute a function in parallel over an iterable with progress tracking.
99
87
 
100
88
  Args:
101
89
  func (Callable[[T], U]): The function to execute on each item in the
@@ -112,6 +100,7 @@ def parallel_progress(
112
100
  Returns:
113
101
  list[U]: A list of results from applying the function to each item in
114
102
  the iterable.
103
+
115
104
  """
116
105
  iterable = list(iterable)
117
106
  total = len(iterable)
@@ -130,8 +119,7 @@ def multi_tasks_progress(
130
119
  transient: bool | None = None,
131
120
  **kwargs,
132
121
  ) -> None:
133
- """
134
- Render auto-updating progress bars for multiple tasks concurrently.
122
+ """Render auto-updating progress bars for multiple tasks concurrently.
135
123
 
136
124
  Args:
137
125
  iterables (Iterable[Iterable[int | tuple[int, int]]]): A collection of
@@ -151,6 +139,7 @@ def multi_tasks_progress(
151
139
 
152
140
  Returns:
153
141
  None
142
+
154
143
  """
155
144
  if not columns:
156
145
  columns = Progress.get_default_columns()
@@ -1,6 +1,6 @@
1
- """
2
- Provide functionality for managing and interacting with MLflow runs.
3
- It includes the `RunCollection` class, which serves as a container
1
+ """Provide a collection of MLflow runs.
2
+
3
+ This module includes the `RunCollection` class, which serves as a container
4
4
  for multiple MLflow `Run` instances, and various methods to filter and
5
5
  retrieve these runs.
6
6
 
@@ -23,6 +23,9 @@ from dataclasses import dataclass, field
23
23
  from itertools import chain
24
24
  from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, overload
25
25
 
26
+ from mlflow.entities import RunStatus
27
+
28
+ import hydraflow.param
26
29
  from hydraflow.config import iter_params
27
30
  from hydraflow.info import RunCollectionInfo
28
31
 
@@ -41,8 +44,7 @@ P = ParamSpec("P")
41
44
 
42
45
  @dataclass
43
46
  class RunCollection:
44
- """
45
- Represent a collection of MLflow runs.
47
+ """Represent a collection of MLflow runs.
46
48
 
47
49
  Provide methods to interact with the runs, such as filtering,
48
50
  retrieving specific runs, and accessing run information.
@@ -86,10 +88,12 @@ class RunCollection:
86
88
  def __contains__(self, run: Run) -> bool:
87
89
  return run in self._runs
88
90
 
91
+ def __bool__(self) -> bool:
92
+ return bool(self._runs)
93
+
89
94
  @classmethod
90
95
  def from_list(cls, runs: list[Run]) -> RunCollection:
91
96
  """Create a `RunCollection` instance from a list of MLflow `Run` instances."""
92
-
93
97
  return cls(runs)
94
98
 
95
99
  @property
@@ -110,6 +114,7 @@ class RunCollection:
110
114
  Returns:
111
115
  A new `RunCollection` instance containing the first n runs if n is
112
116
  positive, or the last n runs if n is negative.
117
+
113
118
  """
114
119
  if n < 0:
115
120
  return self.__class__(self._runs[n:])
@@ -122,17 +127,28 @@ class RunCollection:
122
127
  *,
123
128
  reverse: bool = False,
124
129
  ) -> None:
130
+ """Sort the runs in the collection.
131
+
132
+ Sort the runs in the collection according to the provided key function
133
+ and optional reverse flag.
134
+
135
+ Args:
136
+ key (Callable[[Run], Any] | None): A function that takes a run and returns
137
+ a value to sort by.
138
+ reverse (bool): If True, sort in descending order.
139
+
140
+ """
125
141
  self._runs.sort(key=key or (lambda x: x.info.start_time), reverse=reverse)
126
142
 
127
143
  def one(self) -> Run:
128
- """
129
- Get the only `Run` instance in the collection.
144
+ """Get the only `Run` instance in the collection.
130
145
 
131
146
  Returns:
132
147
  The only `Run` instance in the collection.
133
148
 
134
149
  Raises:
135
150
  ValueError: If the collection does not contain exactly one run.
151
+
136
152
  """
137
153
  if len(self._runs) != 1:
138
154
  raise ValueError("The collection does not contain exactly one run.")
@@ -140,24 +156,24 @@ class RunCollection:
140
156
  return self._runs[0]
141
157
 
142
158
  def try_one(self) -> Run | None:
143
- """
144
- Try to get the only `Run` instance in the collection.
159
+ """Try to get the only `Run` instance in the collection.
145
160
 
146
161
  Returns:
147
162
  The only `Run` instance in the collection, or None if the collection
148
163
  does not contain exactly one run.
164
+
149
165
  """
150
166
  return self._runs[0] if len(self._runs) == 1 else None
151
167
 
152
168
  def first(self) -> Run:
153
- """
154
- Get the first `Run` instance in the collection.
169
+ """Get the first `Run` instance in the collection.
155
170
 
156
171
  Returns:
157
172
  The first `Run` instance in the collection.
158
173
 
159
174
  Raises:
160
175
  ValueError: If the collection is empty.
176
+
161
177
  """
162
178
  if not self._runs:
163
179
  raise ValueError("The collection is empty.")
@@ -165,24 +181,24 @@ class RunCollection:
165
181
  return self._runs[0]
166
182
 
167
183
  def try_first(self) -> Run | None:
168
- """
169
- Try to get the first `Run` instance in the collection.
184
+ """Try to get the first `Run` instance in the collection.
170
185
 
171
186
  Returns:
172
187
  The first `Run` instance in the collection, or None if the collection
173
188
  is empty.
189
+
174
190
  """
175
191
  return self._runs[0] if self._runs else None
176
192
 
177
193
  def last(self) -> Run:
178
- """
179
- Get the last `Run` instance in the collection.
194
+ """Get the last `Run` instance in the collection.
180
195
 
181
196
  Returns:
182
197
  The last `Run` instance in the collection.
183
198
 
184
199
  Raises:
185
200
  ValueError: If the collection is empty.
201
+
186
202
  """
187
203
  if not self._runs:
188
204
  raise ValueError("The collection is empty.")
@@ -190,18 +206,17 @@ class RunCollection:
190
206
  return self._runs[-1]
191
207
 
192
208
  def try_last(self) -> Run | None:
193
- """
194
- Try to get the last `Run` instance in the collection.
209
+ """Try to get the last `Run` instance in the collection.
195
210
 
196
211
  Returns:
197
212
  The last `Run` instance in the collection, or None if the collection
198
213
  is empty.
214
+
199
215
  """
200
216
  return self._runs[-1] if self._runs else None
201
217
 
202
218
  def filter(self, config: object | None = None, **kwargs) -> RunCollection:
203
- """
204
- Filter the `Run` instances based on the provided configuration.
219
+ """Filter the `Run` instances based on the provided configuration.
205
220
 
206
221
  This method filters the runs in the collection according to the
207
222
  specified configuration object and additional key-value pairs. The
@@ -224,12 +239,12 @@ class RunCollection:
224
239
 
225
240
  Returns:
226
241
  A new `RunCollection` object containing the filtered runs.
242
+
227
243
  """
228
244
  return RunCollection(filter_runs(self._runs, config, **kwargs))
229
245
 
230
246
  def find(self, config: object | None = None, **kwargs) -> Run:
231
- """
232
- Find the first `Run` instance based on the provided configuration.
247
+ """Find the first `Run` instance based on the provided configuration.
233
248
 
234
249
  This method filters the runs in the collection according to the
235
250
  specified configuration object and returns the first run that matches
@@ -248,6 +263,7 @@ class RunCollection:
248
263
 
249
264
  See Also:
250
265
  `filter`: Perform the actual filtering logic.
266
+
251
267
  """
252
268
  try:
253
269
  return self.filter(config, **kwargs).first()
@@ -255,8 +271,7 @@ class RunCollection:
255
271
  raise ValueError("No run matches the provided configuration.")
256
272
 
257
273
  def try_find(self, config: object | None = None, **kwargs) -> Run | None:
258
- """
259
- Try to find the first `Run` instance based on the provided configuration.
274
+ """Try to find the first `Run` instance based on the provided configuration.
260
275
 
261
276
  This method filters the runs in the collection according to the
262
277
  specified configuration object and returns the first run that matches
@@ -273,12 +288,12 @@ class RunCollection:
273
288
 
274
289
  See Also:
275
290
  `filter`: Perform the actual filtering logic.
291
+
276
292
  """
277
293
  return self.filter(config, **kwargs).try_first()
278
294
 
279
295
  def find_last(self, config: object | None = None, **kwargs) -> Run:
280
- """
281
- Find the last `Run` instance based on the provided configuration.
296
+ """Find the last `Run` instance based on the provided configuration.
282
297
 
283
298
  This method filters the runs in the collection according to the
284
299
  specified configuration object and returns the last run that matches
@@ -297,6 +312,7 @@ class RunCollection:
297
312
 
298
313
  See Also:
299
314
  `filter`: Perform the actual filtering logic.
315
+
300
316
  """
301
317
  try:
302
318
  return self.filter(config, **kwargs).last()
@@ -304,8 +320,7 @@ class RunCollection:
304
320
  raise ValueError("No run matches the provided configuration.")
305
321
 
306
322
  def try_find_last(self, config: object | None = None, **kwargs) -> Run | None:
307
- """
308
- Try to find the last `Run` instance based on the provided configuration.
323
+ """Try to find the last `Run` instance based on the provided configuration.
309
324
 
310
325
  This method filters the runs in the collection according to the
311
326
  specified configuration object and returns the last run that matches
@@ -322,12 +337,12 @@ class RunCollection:
322
337
 
323
338
  See Also:
324
339
  `filter`: Perform the actual filtering logic.
340
+
325
341
  """
326
342
  return self.filter(config, **kwargs).try_last()
327
343
 
328
344
  def get(self, config: object | None = None, **kwargs) -> Run:
329
- """
330
- Retrieve a specific `Run` instance based on the provided configuration.
345
+ """Retrieve a specific `Run` instance based on the provided configuration.
331
346
 
332
347
  This method filters the runs in the collection according to the
333
348
  specified configuration object and returns the run that matches the
@@ -347,6 +362,7 @@ class RunCollection:
347
362
 
348
363
  See Also:
349
364
  `filter`: Perform the actual filtering logic.
365
+
350
366
  """
351
367
  try:
352
368
  return self.filter(config, **kwargs).one()
@@ -355,8 +371,7 @@ class RunCollection:
355
371
  raise ValueError(msg)
356
372
 
357
373
  def try_get(self, config: object | None = None, **kwargs) -> Run | None:
358
- """
359
- Try to retrieve a specific `Run` instance based on the provided configuration.
374
+ """Try to retrieve a specific `Run` instance based on the provided config.
360
375
 
361
376
  This method filters the runs in the collection according to the
362
377
  specified configuration object and returns the run that matches the
@@ -376,12 +391,12 @@ class RunCollection:
376
391
 
377
392
  See Also:
378
393
  `filter`: Perform the actual filtering logic.
394
+
379
395
  """
380
396
  return self.filter(config, **kwargs).try_one()
381
397
 
382
398
  def get_param_names(self) -> list[str]:
383
- """
384
- Get the parameter names from the runs.
399
+ """Get the parameter names from the runs.
385
400
 
386
401
  This method extracts the unique parameter names from the provided list
387
402
  of runs. It iterates through each run and collects the parameter names
@@ -389,6 +404,7 @@ class RunCollection:
389
404
 
390
405
  Returns:
391
406
  A list of unique parameter names.
407
+
392
408
  """
393
409
  param_names = set()
394
410
 
@@ -398,24 +414,30 @@ class RunCollection:
398
414
 
399
415
  return list(param_names)
400
416
 
401
- def get_param_dict(self) -> dict[str, list[str]]:
402
- """
403
- Get the parameter dictionary from the list of runs.
417
+ def get_param_dict(self, *, drop_const: bool = False) -> dict[str, list[str]]:
418
+ """Get the parameter dictionary from the list of runs.
404
419
 
405
420
  This method extracts the parameter names and their corresponding values
406
421
  from the provided list of runs. It iterates through each run and
407
422
  collects the parameter values into a dictionary where the keys are
408
423
  parameter names and the values are lists of parameter values.
409
424
 
425
+ Args:
426
+ drop_const (bool): If True, drop the parameter values that are constant
427
+ across all runs.
428
+
410
429
  Returns:
411
430
  A dictionary where the keys are parameter names and the values are
412
431
  lists of parameter values.
432
+
413
433
  """
414
434
  params = {}
415
435
 
416
436
  for name in self.get_param_names():
417
437
  it = (run.data.params[name] for run in self if name in run.data.params)
418
- params[name] = sorted(set(it))
438
+ unique_values = sorted(set(it))
439
+ if not drop_const or len(unique_values) > 1:
440
+ params[name] = unique_values
419
441
 
420
442
  return params
421
443
 
@@ -425,9 +447,7 @@ class RunCollection:
425
447
  *args: P.args,
426
448
  **kwargs: P.kwargs,
427
449
  ) -> Iterator[T]:
428
- """
429
- Apply a function to each run in the collection and return an iterator of
430
- results.
450
+ """Return an iterator of results by applying a function to each run.
431
451
 
432
452
  This method iterates over each run in the collection and applies the
433
453
  provided function to it, along with any additional arguments and
@@ -441,6 +461,7 @@ class RunCollection:
441
461
 
442
462
  Yields:
443
463
  Results obtained by applying the function to each run in the collection.
464
+
444
465
  """
445
466
  return (func(run, *args, **kwargs) for run in self)
446
467
 
@@ -450,9 +471,7 @@ class RunCollection:
450
471
  *args: P.args,
451
472
  **kwargs: P.kwargs,
452
473
  ) -> Iterator[T]:
453
- """
454
- Apply a function to each run id in the collection and return an iterator
455
- of results.
474
+ """Return an iterator of results by applying a function to each run id.
456
475
 
457
476
  Args:
458
477
  func (Callable[[str, P], T]): A function that takes a run id and returns a
@@ -463,6 +482,7 @@ class RunCollection:
463
482
  Yields:
464
483
  Results obtained by applying the function to each run id in the
465
484
  collection.
485
+
466
486
  """
467
487
  return (func(run_id, *args, **kwargs) for run_id in self.info.run_id)
468
488
 
@@ -472,9 +492,7 @@ class RunCollection:
472
492
  *args: P.args,
473
493
  **kwargs: P.kwargs,
474
494
  ) -> Iterator[T]:
475
- """
476
- Apply a function to each run configuration in the collection and return
477
- an iterator of results.
495
+ """Return an iterator of results by applying a function to each run config.
478
496
 
479
497
  Args:
480
498
  func (Callable[[DictConfig, P], T]): A function that takes a run
@@ -485,6 +503,7 @@ class RunCollection:
485
503
  Yields:
486
504
  Results obtained by applying the function to each run configuration
487
505
  in the collection.
506
+
488
507
  """
489
508
  return (func(config, *args, **kwargs) for config in self.info.config)
490
509
 
@@ -494,9 +513,7 @@ class RunCollection:
494
513
  *args: P.args,
495
514
  **kwargs: P.kwargs,
496
515
  ) -> Iterator[T]:
497
- """
498
- Apply a function to each artifact URI in the collection and return an
499
- iterator of results.
516
+ """Return an iterator of results by applying a function to each artifact URI.
500
517
 
501
518
  Iterate over each run in the collection, retrieves the artifact URI, and
502
519
  apply the provided function to it. If a run does not have an artifact
@@ -511,6 +528,7 @@ class RunCollection:
511
528
  Yields:
512
529
  Results obtained by applying the function to each artifact URI in the
513
530
  collection.
531
+
514
532
  """
515
533
  return (func(uri, *args, **kwargs) for uri in self.info.artifact_uri)
516
534
 
@@ -520,9 +538,7 @@ class RunCollection:
520
538
  *args: P.args,
521
539
  **kwargs: P.kwargs,
522
540
  ) -> Iterator[T]:
523
- """
524
- Apply a function to each artifact directory in the collection and return
525
- an iterator of results.
541
+ """Return an iterator of results by applying a function to each artifact dir.
526
542
 
527
543
  Iterate over each run in the collection, downloads the artifact
528
544
  directory, and apply the provided function to the directory path.
@@ -536,6 +552,7 @@ class RunCollection:
536
552
  Yields:
537
553
  Results obtained by applying the function to each artifact directory
538
554
  in the collection.
555
+
539
556
  """
540
557
  return (func(dir, *args, **kwargs) for dir in self.info.artifact_dir) # noqa: A001
541
558
 
@@ -543,8 +560,7 @@ class RunCollection:
543
560
  self,
544
561
  *names: str | list[str],
545
562
  ) -> dict[tuple[str | None, ...], RunCollection]:
546
- """
547
- Group runs by specified parameter names.
563
+ """Group runs by specified parameter names.
548
564
 
549
565
  Group the runs in the collection based on the values of the
550
566
  specified parameters. Each unique combination of parameter values will
@@ -559,6 +575,7 @@ class RunCollection:
559
575
  dict[tuple[str | None, ...], RunCollection]: A dictionary where the keys
560
576
  are tuples of parameter values and the values are RunCollection objects
561
577
  containing the runs that match those parameter values.
578
+
562
579
  """
563
580
  grouped_runs: dict[tuple[str | None, ...], list[Run]] = {}
564
581
  for run in self._runs:
@@ -569,48 +586,25 @@ class RunCollection:
569
586
 
570
587
 
571
588
  def _param_matches(run: Run, key: str, value: Any) -> bool:
572
- """
573
- Check if the run's parameter matches the specified key-value pair.
574
-
575
- Check if the run's parameters contain the specified
576
- key-value pair. It handles different types of values, including lists
577
- and tuples.
578
-
579
- Args:
580
- run (Run): The run object to check.
581
- key (str): The parameter key to check.
582
- value (Any): The parameter value to check.
583
-
584
- Returns:
585
- True if the run's parameter matches the specified key-value pair,
586
- False otherwise.
587
- """
588
- param = run.data.params.get(key, value)
589
-
590
- if param is None:
591
- return False
589
+ params = run.data.params
590
+ if key not in params:
591
+ return True
592
592
 
593
+ param = params[key]
593
594
  if param == "None":
594
- return value is None
595
+ return value is None or value == "None"
595
596
 
596
- if isinstance(value, list) and value:
597
- return type(value[0])(param) in value
598
-
599
- if isinstance(value, tuple) and len(value) == 2: # noqa: PLR2004
600
- return value[0] <= type(value[0])(param) < value[1]
601
-
602
- return type(value)(param) == value
597
+ return hydraflow.param.match(param, value)
603
598
 
604
599
 
605
600
  def filter_runs(
606
601
  runs: list[Run],
607
602
  config: object | None = None,
608
603
  *,
609
- status: str | list[str] | None = None,
604
+ status: str | list[str] | int | list[int] | None = None,
610
605
  **kwargs,
611
606
  ) -> list[Run]:
612
- """
613
- Filter the runs based on the provided configuration.
607
+ """Filter the runs based on the provided configuration.
614
608
 
615
609
  Filter the runs in the collection according to the
616
610
  specified configuration object and additional key-value pairs.
@@ -630,33 +624,61 @@ def filter_runs(
630
624
  config (object | None): The configuration object to filter the runs.
631
625
  This can be any object that provides key-value pairs through the
632
626
  `iter_params` function.
633
- status (str | list[str] | None): The status of the runs to filter.
627
+ status (str | list[str] | RunStatus | list[RunStatus] | None): The status of
628
+ the runs to filter.
634
629
  **kwargs: Additional key-value pairs to filter the runs.
635
630
 
636
631
  Returns:
637
632
  A list of runs that match the specified configuration and key-value pairs.
633
+
638
634
  """
639
635
  for key, value in chain(iter_params(config), kwargs.items()):
640
636
  runs = [run for run in runs if _param_matches(run, key, value)]
641
637
 
642
- if len(runs) == 0:
643
- return []
638
+ if len(runs) == 0 or status is None:
639
+ return runs
644
640
 
645
- if isinstance(status, str) and status.startswith("!"):
646
- status = status[1:].lower()
647
- return [run for run in runs if run.info.status.lower() != status]
641
+ return filter_runs_by_status(runs, status)
648
642
 
649
- if status:
650
- status = [status] if isinstance(status, str) else status
651
- status = [s.lower() for s in status]
652
- return [run for run in runs if run.info.status.lower() in status]
653
643
 
654
- return runs
644
+ def filter_runs_by_status(
645
+ runs: list[Run],
646
+ status: str | list[str] | int | list[int],
647
+ ) -> list[Run]:
648
+ """Filter the runs based on the provided status.
655
649
 
650
+ Args:
651
+ runs (list[Run]): The list of runs to filter.
652
+ status (str | list[str] | int | list[int]): The status of the runs
653
+ to filter.
654
+
655
+ Returns:
656
+ A list of runs that match the specified status.
656
657
 
657
- def get_params(run: Run, *names: str | list[str]) -> tuple[str | None, ...]:
658
658
  """
659
- Retrieve the values of specified parameters from the given run.
659
+ if isinstance(status, str):
660
+ if status.startswith("!"):
661
+ status = status[1:].lower()
662
+ return [run for run in runs if run.info.status.lower() != status]
663
+
664
+ status = [status]
665
+
666
+ elif isinstance(status, int):
667
+ status = [RunStatus.to_string(status)]
668
+
669
+ status = [_to_lower(s) for s in status]
670
+ return [run for run in runs if run.info.status.lower() in status]
671
+
672
+
673
+ def _to_lower(status: str | int) -> str:
674
+ if isinstance(status, str):
675
+ return status.lower()
676
+
677
+ return RunStatus.to_string(status).lower()
678
+
679
+
680
+ def get_params(run: Run, *names: str | list[str]) -> tuple[str | None, ...]:
681
+ """Retrieve the values of specified parameters from the given run.
660
682
 
661
683
  This function extracts the values of the parameters identified by the
662
684
  provided names from the specified run. It can accept both individual
@@ -671,6 +693,7 @@ def get_params(run: Run, *names: str | list[str]) -> tuple[str | None, ...]:
671
693
  Returns:
672
694
  tuple[str | None, ...]: A tuple containing the values of the specified
673
695
  parameters in the order they were provided.
696
+
674
697
  """
675
698
  names_ = []
676
699
  for name in names:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: hydraflow
3
- Version: 0.2.16
3
+ Version: 0.2.18
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,14 @@
1
+ hydraflow/__init__.py,sha256=B7rWSiGP5WwWjijcb41Bv9uuo5MQ6gbBbVWGAWYtK-k,598
2
+ hydraflow/asyncio.py,sha256=-i1C8KAmNDImrjHnk92Csaa1mpjdK8Vp4ZVaQV-l94s,6634
3
+ hydraflow/config.py,sha256=sBaEYPMAGSIOc_wdDsWm0k4y3AZyWIET8gqa_o95SDA,2089
4
+ hydraflow/context.py,sha256=ih_jnexaHoToNq1dZ6sBzhJWFluPiQluOlYTYOzNEgk,8222
5
+ hydraflow/info.py,sha256=Vzyz9dEWcU9ovRG3JWshxIazzod1cZoHF74bHhHL3AI,3946
6
+ hydraflow/mlflow.py,sha256=GkOr_pXfpfY5USYBLrCigHcP13VgrAK_e9kheR1Wke4,8579
7
+ hydraflow/param.py,sha256=dvIXcKgc_MPiju3WEk9qz5FOUeK5qSj-YWN2ophCpUM,1938
8
+ hydraflow/progress.py,sha256=zvKX1HCN8_xDOsgYOEcLLhkhdPdep-U8vHrc0XZ-6SQ,6163
9
+ hydraflow/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ hydraflow/run_collection.py,sha256=gsseBQ6a2YolNanISgEgkjei7o9U6ZGV-Tk50UYH850,24295
11
+ hydraflow-0.2.18.dist-info/METADATA,sha256=roL3lGtlIibF6rHbCp4aXrCphhq-OkNe0JwLxM1xtBY,3819
12
+ hydraflow-0.2.18.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
13
+ hydraflow-0.2.18.dist-info/licenses/LICENSE,sha256=IGdDrBPqz1O0v_UwCW-NJlbX9Hy9b3uJ11t28y2srmY,1062
14
+ hydraflow-0.2.18.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- hydraflow/__init__.py,sha256=B7rWSiGP5WwWjijcb41Bv9uuo5MQ6gbBbVWGAWYtK-k,598
2
- hydraflow/asyncio.py,sha256=eFnDbNOQ5Hmjdforr8rTW6i_rr-zFIVY3xSQQ45gMPA,6511
3
- hydraflow/config.py,sha256=YU6xYLinxq-Iqw1R3Zy7s3_u8nfpvnvXlGIkPXJTNLc,2116
4
- hydraflow/context.py,sha256=4UDaWGoVmeF36UqsKoh6dd_cS_YVRfz80gFr28ouNlo,8040
5
- hydraflow/info.py,sha256=7EsCMEH6LJZB3FZiQ3IpPFTD3Meaz7G3M-HvDQeo1rw,3466
6
- hydraflow/mlflow.py,sha256=irD1INrVaI_1RIzUCjI36voBqgZszZ4dkSLo4aT1_FM,8271
7
- hydraflow/progress.py,sha256=b5LvLm3d0eW3WsaidZAZotJNTTN3OwSY3XwxXXsJV9A,6561
8
- hydraflow/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- hydraflow/run_collection.py,sha256=tiNKy_mUmE-9moLs7enfLQyiwTUvL5eCKnD1acKeUFw,23854
10
- hydraflow-0.2.16.dist-info/METADATA,sha256=PUsFQ8YLW_L-rVzzx1OzQX6imjdQglhIAgJCSV9qEaM,3819
11
- hydraflow-0.2.16.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
12
- hydraflow-0.2.16.dist-info/licenses/LICENSE,sha256=IGdDrBPqz1O0v_UwCW-NJlbX9Hy9b3uJ11t28y2srmY,1062
13
- hydraflow-0.2.16.dist-info/RECORD,,