hydraflow 0.1.0__py3-none-any.whl → 0.1.2__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,24 +1,24 @@
1
1
  from .context import Info, chdir_artifact, log_run, watch
2
2
  from .mlflow import set_experiment
3
- from .run import (
4
- filter_by_config,
3
+ from .runs import (
4
+ filter_runs,
5
5
  get_artifact_dir,
6
6
  get_artifact_path,
7
7
  get_artifact_uri,
8
- get_by_config,
9
8
  get_param_dict,
10
9
  get_param_names,
10
+ get_run,
11
11
  get_run_id,
12
12
  )
13
13
 
14
14
  __all__ = [
15
15
  "Info",
16
16
  "chdir_artifact",
17
- "filter_by_config",
17
+ "filter_runs",
18
18
  "get_artifact_dir",
19
19
  "get_artifact_path",
20
20
  "get_artifact_uri",
21
- "get_by_config",
21
+ "get_run",
22
22
  "get_param_dict",
23
23
  "get_param_names",
24
24
  "get_run_id",
hydraflow/context.py CHANGED
@@ -13,7 +13,7 @@ from watchdog.events import FileModifiedEvent, FileSystemEventHandler
13
13
  from watchdog.observers import Observer
14
14
 
15
15
  from hydraflow.mlflow import log_params
16
- from hydraflow.run import get_artifact_path
16
+ from hydraflow.runs import get_artifact_path
17
17
  from hydraflow.util import uri_to_path
18
18
 
19
19
  if TYPE_CHECKING:
@@ -40,14 +40,19 @@ def log_run(
40
40
  hc = HydraConfig.get()
41
41
  output_dir = Path(hc.runtime.output_dir)
42
42
  uri = mlflow.get_artifact_uri()
43
- location = Info(output_dir, uri_to_path(uri))
43
+ info = Info(output_dir, uri_to_path(uri))
44
44
 
45
45
  # Save '.hydra' config directory first.
46
46
  output_subdir = output_dir / (hc.output_subdir or "")
47
47
  mlflow.log_artifacts(output_subdir.as_posix(), hc.output_subdir)
48
48
 
49
+ def log_artifact(path: Path) -> None:
50
+ local_path = (output_dir / path).as_posix()
51
+ mlflow.log_artifact(local_path)
52
+
49
53
  try:
50
- yield location
54
+ with watch(log_artifact, output_dir):
55
+ yield info
51
56
 
52
57
  finally:
53
58
  # Save output_dir including '.hydra' config directory.
@@ -55,11 +60,7 @@ def log_run(
55
60
 
56
61
 
57
62
  @contextmanager
58
- def watch(
59
- func: Callable[[Path], None],
60
- dir: Path | str = "",
61
- timeout: int = 600,
62
- ) -> Iterator[None]:
63
+ def watch(func: Callable[[Path], None], dir: Path | str = "", timeout: int = 60) -> Iterator[None]:
63
64
  if not dir:
64
65
  uri = mlflow.get_artifact_uri()
65
66
  dir = uri_to_path(uri)
hydraflow/mlflow.py CHANGED
@@ -6,10 +6,13 @@ from hydra.core.hydra_config import HydraConfig
6
6
  from hydraflow.config import iter_params
7
7
 
8
8
 
9
- def set_experiment() -> None:
9
+ def set_experiment(prefix: str = "", suffix: str = "", uri: str | None = None) -> None:
10
+ if uri:
11
+ mlflow.set_tracking_uri(uri)
12
+
10
13
  hc = HydraConfig.get()
11
- mlflow.set_tracking_uri("")
12
- mlflow.set_experiment(hc.job.name)
14
+ name = f"{prefix}{hc.job.name}{suffix}"
15
+ mlflow.set_experiment(name)
13
16
 
14
17
 
15
18
  def log_params(config: object, *, synchronous: bool | None = None) -> None:
hydraflow/runs.py ADDED
@@ -0,0 +1,217 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from functools import cache
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ import mlflow
9
+ import numpy as np
10
+ from mlflow.entities.run import Run as Run_
11
+ from mlflow.tracking import artifact_utils
12
+ from omegaconf import DictConfig, OmegaConf
13
+ from pandas import DataFrame, Series
14
+
15
+ from hydraflow.config import iter_params
16
+ from hydraflow.util import uri_to_path
17
+
18
+ if TYPE_CHECKING:
19
+ from typing import Any
20
+
21
+
22
+ @dataclass
23
+ class Runs:
24
+ runs: list[Run_] | DataFrame
25
+
26
+ def __repr__(self) -> str:
27
+ return f"{self.__class__.__name__}({len(self)})"
28
+
29
+ def __len__(self) -> int:
30
+ return len(self.runs)
31
+
32
+ def filter(self, config: object) -> Runs:
33
+ return Runs(filter_runs(self.runs, config))
34
+
35
+ def get(self, config: object) -> Run:
36
+ return Run(get_run(self.runs, config))
37
+
38
+ def drop_unique_params(self) -> Runs:
39
+ if isinstance(self.runs, DataFrame):
40
+ return Runs(drop_unique_params(self.runs))
41
+
42
+ raise NotImplementedError
43
+
44
+ def get_param_names(self) -> list[str]:
45
+ if isinstance(self.runs, DataFrame):
46
+ return get_param_names(self.runs)
47
+
48
+ raise NotImplementedError
49
+
50
+ def get_param_dict(self) -> dict[str, list[str]]:
51
+ if isinstance(self.runs, DataFrame):
52
+ return get_param_dict(self.runs)
53
+
54
+ raise NotImplementedError
55
+
56
+
57
+ def filter_runs(runs: list[Run_] | DataFrame, config: object) -> list[Run_] | DataFrame:
58
+ if isinstance(runs, list):
59
+ return filter_runs_list(runs, config)
60
+
61
+ return filter_runs_dataframe(runs, config)
62
+
63
+
64
+ def _is_equal(run: Run_, key: str, value: Any) -> bool:
65
+ param = run.data.params.get(key, value)
66
+
67
+ if param is None:
68
+ return False
69
+
70
+ return type(value)(param) == value
71
+
72
+
73
+ def filter_runs_list(runs: list[Run_], config: object) -> list[Run_]:
74
+ for key, value in iter_params(config):
75
+ runs = [run for run in runs if _is_equal(run, key, value)]
76
+
77
+ return runs
78
+
79
+
80
+ def filter_runs_dataframe(runs: DataFrame, config: object) -> DataFrame:
81
+ index = np.ones(len(runs), dtype=bool)
82
+
83
+ for key, value in iter_params(config):
84
+ name = f"params.{key}"
85
+
86
+ if name in runs:
87
+ series = runs[name]
88
+ is_value = -series.isna()
89
+ param = series.fillna(value).astype(type(value))
90
+ index &= is_value & (param == value)
91
+
92
+ return runs[index]
93
+
94
+
95
+ def get_run(runs: list[Run_] | DataFrame, config: object) -> Run_ | Series:
96
+ runs = filter_runs(runs, config)
97
+
98
+ if len(runs) == 1:
99
+ return runs[0] if isinstance(runs, list) else runs.iloc[0]
100
+
101
+ msg = f"number of filtered runs is not 1: got {len(runs)}"
102
+ raise ValueError(msg)
103
+
104
+
105
+ def drop_unique_params(runs: DataFrame) -> DataFrame:
106
+ def select(column: str) -> bool:
107
+ return not column.startswith("params.") or len(runs[column].unique()) > 1
108
+
109
+ columns = [select(column) for column in runs.columns]
110
+ return runs.iloc[:, columns]
111
+
112
+
113
+ def get_param_names(runs: DataFrame) -> list[str]:
114
+ def get_name(column: str) -> str:
115
+ if column.startswith("params."):
116
+ return column.split(".", maxsplit=1)[-1]
117
+
118
+ return ""
119
+
120
+ columns = [get_name(column) for column in runs.columns]
121
+ return [column for column in columns if column]
122
+
123
+
124
+ def get_param_dict(runs: DataFrame) -> dict[str, list[str]]:
125
+ params = {}
126
+ for name in get_param_names(runs):
127
+ params[name] = list(runs[f"params.{name}"].unique())
128
+
129
+ return params
130
+
131
+
132
+ @dataclass
133
+ class Run:
134
+ run: Run_ | Series | str
135
+
136
+ def __repr__(self) -> str:
137
+ return f"{self.__class__.__name__}({self.run_id!r})"
138
+
139
+ @property
140
+ def run_id(self) -> str:
141
+ return get_run_id(self.run)
142
+
143
+ def artifact_uri(self, artifact_path: str | None = None) -> str:
144
+ return get_artifact_uri(self.run, artifact_path)
145
+
146
+ @property
147
+ def artifact_dir(self) -> Path:
148
+ return get_artifact_dir(self.run)
149
+
150
+ def artifact_path(self, artifact_path: str | None = None) -> Path:
151
+ return get_artifact_path(self.run, artifact_path)
152
+
153
+ @property
154
+ def config(self) -> DictConfig:
155
+ return load_config(self.run)
156
+
157
+ def log_hydra_output_dir(self) -> None:
158
+ log_hydra_output_dir(self.run)
159
+
160
+
161
+ def get_run_id(run: Run_ | Series | str) -> str:
162
+ if isinstance(run, str):
163
+ return run
164
+
165
+ if isinstance(run, Run_):
166
+ return run.info.run_id
167
+
168
+ return run.run_id
169
+
170
+
171
+ def get_artifact_uri(run: Run_ | Series | str, artifact_path: str | None = None) -> str:
172
+ run_id = get_run_id(run)
173
+ return artifact_utils.get_artifact_uri(run_id, artifact_path)
174
+
175
+
176
+ def get_artifact_dir(run: Run_ | Series | str) -> Path:
177
+ uri = get_artifact_uri(run)
178
+ return uri_to_path(uri)
179
+
180
+
181
+ def get_artifact_path(run: Run_ | Series | str, artifact_path: str | None = None) -> Path:
182
+ artifact_dir = get_artifact_dir(run)
183
+ return artifact_dir / artifact_path if artifact_path else artifact_dir
184
+
185
+
186
+ def load_config(run: Run_ | Series | str) -> DictConfig:
187
+ run_id = get_run_id(run)
188
+ return _load_config(run_id)
189
+
190
+
191
+ @cache
192
+ def _load_config(run_id: str) -> DictConfig:
193
+ try:
194
+ path = mlflow.artifacts.download_artifacts(
195
+ run_id=run_id,
196
+ artifact_path=".hydra/config.yaml",
197
+ )
198
+ except OSError:
199
+ return DictConfig({})
200
+
201
+ return OmegaConf.load(path) # type: ignore
202
+
203
+
204
+ def get_hydra_output_dir(run: Run_ | Series | str) -> Path:
205
+ path = get_artifact_dir(run) / ".hydra/hydra.yaml"
206
+
207
+ if path.exists():
208
+ hc = OmegaConf.load(path)
209
+ return Path(hc.hydra.runtime.output_dir)
210
+
211
+ raise FileNotFoundError
212
+
213
+
214
+ def log_hydra_output_dir(run: Run_ | Series | str) -> None:
215
+ output_dir = get_hydra_output_dir(run)
216
+ run_id = run if isinstance(run, str) else run.info.run_id
217
+ mlflow.log_artifacts(output_dir.as_posix(), run_id=run_id)
hydraflow/util.py CHANGED
@@ -1,11 +1,11 @@
1
- import platform
2
- from pathlib import Path
3
- from urllib.parse import urlparse
4
-
5
-
6
- def uri_to_path(uri: str) -> Path:
7
- path = urlparse(uri).path
8
- if platform.system() == "Windows" and path.startswith("/"):
9
- path = path[1:]
10
-
11
- return Path(path)
1
+ import platform
2
+ from pathlib import Path
3
+ from urllib.parse import urlparse
4
+
5
+
6
+ def uri_to_path(uri: str) -> Path:
7
+ path = urlparse(uri).path
8
+ if platform.system() == "Windows" and path.startswith("/"):
9
+ path = path[1:]
10
+
11
+ return Path(path)
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.3
2
+ Name: hydraflow
3
+ Version: 0.1.2
4
+ Summary: Hydra with MLflow
5
+ Project-URL: Documentation, https://github.com/daizutabi/hydraflow
6
+ Project-URL: Source, https://github.com/daizutabi/hydraflow
7
+ Project-URL: Issues, https://github.com/daizutabi/hydraflow/issues
8
+ Author-email: daizutabi <daizutabi@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Documentation
17
+ Classifier: Topic :: Software Development :: Documentation
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: hydra-core>1.3
20
+ Requires-Dist: mlflow>2.15
21
+ Requires-Dist: setuptools
22
+ Requires-Dist: watchdog
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest-clarity; extra == 'dev'
25
+ Requires-Dist: pytest-cov; extra == 'dev'
26
+ Requires-Dist: pytest-randomly; extra == 'dev'
27
+ Requires-Dist: pytest-xdist; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # hydraflow
31
+
32
+ [![PyPI Version][pypi-v-image]][pypi-v-link]
33
+ [![Python Version][python-v-image]][python-v-link]
34
+ [![Build Status][GHAction-image]][GHAction-link]
35
+ [![Coverage Status][codecov-image]][codecov-link]
36
+
37
+ <!-- Badges -->
38
+ [pypi-v-image]: https://img.shields.io/pypi/v/hydraflow.svg
39
+ [pypi-v-link]: https://pypi.org/project/hydraflow/
40
+ [python-v-image]: https://img.shields.io/pypi/pyversions/hydraflow.svg
41
+ [python-v-link]: https://pypi.org/project/hydraflow
42
+ [GHAction-image]: https://github.com/daizutabi/hydraflow/actions/workflows/ci.yml/badge.svg?branch=main&event=push
43
+ [GHAction-link]: https://github.com/daizutabi/hydraflow/actions?query=event%3Apush+branch%3Amain
44
+ [codecov-image]: https://codecov.io/github/daizutabi/hydraflow/coverage.svg?branch=main
45
+ [codecov-link]: https://codecov.io/github/daizutabi/hydraflow?branch=main
@@ -0,0 +1,10 @@
1
+ hydraflow/__init__.py,sha256=REmelavZ1HnRhRDI4NUTkD-28g4QkKmaD5e9QYqJVQg,538
2
+ hydraflow/config.py,sha256=b3Plh_lmq94loZNw9QP2asd6thCLyTzzYSutH0cONXA,964
3
+ hydraflow/context.py,sha256=3vejDbRYQBuBwlhpBpOv5aoyZ-yS8UUzpbCFK1V1uvw,2720
4
+ hydraflow/mlflow.py,sha256=unBP3Y7ujTM3E_Hq_eYvRVFZoGfTA7B0h4FkOZtPPqc,566
5
+ hydraflow/runs.py,sha256=127YykWzmiNUUuJSGPOCZasXmd6tcE15HU32j8x71ck,5864
6
+ hydraflow/util.py,sha256=_BdOMq5tKPm8HOehb2s2ZIBpJYyVpvO_yaAIxbSj51I,253
7
+ hydraflow-0.1.2.dist-info/METADATA,sha256=0cbCow_vMRhuwA6e1ELDCy3pBq6z2Ad1R_qVuGIeqw0,1903
8
+ hydraflow-0.1.2.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
9
+ hydraflow-0.1.2.dist-info/licenses/LICENSE,sha256=IGdDrBPqz1O0v_UwCW-NJlbX9Hy9b3uJ11t28y2srmY,1062
10
+ hydraflow-0.1.2.dist-info/RECORD,,
hydraflow/run.py DELETED
@@ -1,172 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from pathlib import Path
4
- from typing import TYPE_CHECKING, Any, overload
5
-
6
- import mlflow
7
- import numpy as np
8
- from mlflow.entities.run import Run
9
- from mlflow.tracking import artifact_utils
10
- from omegaconf import DictConfig, OmegaConf
11
-
12
- from hydraflow.config import iter_params
13
- from hydraflow.util import uri_to_path
14
-
15
- if TYPE_CHECKING:
16
- from typing import Any
17
-
18
- from pandas import DataFrame, Series
19
-
20
-
21
- @overload
22
- def filter_by_config(runs: list[Run], config: object) -> list[Run]: ...
23
-
24
-
25
- @overload
26
- def filter_by_config(runs: DataFrame, config: object) -> DataFrame: ...
27
-
28
-
29
- def filter_by_config(runs: list[Run] | DataFrame, config: object):
30
- if isinstance(runs, list):
31
- return filter_by_config_list(runs, config)
32
-
33
- return filter_by_config_dataframe(runs, config)
34
-
35
-
36
- def _is_equal(run: Run, key: str, value: Any) -> bool:
37
- param = run.data.params.get(key, value)
38
- if param is None:
39
- return False
40
-
41
- return type(value)(param) == value
42
-
43
-
44
- def filter_by_config_list(runs: list[Run], config: object) -> list[Run]:
45
- for key, value in iter_params(config):
46
- runs = [run for run in runs if _is_equal(run, key, value)]
47
-
48
- return runs
49
-
50
-
51
- def filter_by_config_dataframe(runs: DataFrame, config: object) -> DataFrame:
52
- index = np.ones(len(runs), dtype=bool)
53
-
54
- for key, value in iter_params(config):
55
- name = f"params.{key}"
56
- if name in runs:
57
- series = runs[name]
58
- is_value = -series.isna()
59
- param = series.fillna(value).astype(type(value))
60
- index &= is_value & (param == value)
61
-
62
- return runs[index]
63
-
64
-
65
- @overload
66
- def get_by_config(runs: list[Run], config: object) -> Run: ...
67
-
68
-
69
- @overload
70
- def get_by_config(runs: DataFrame, config: object) -> Series: ...
71
-
72
-
73
- def get_by_config(runs: list[Run] | DataFrame, config: object):
74
- runs = filter_by_config(runs, config)
75
-
76
- if len(runs) == 1:
77
- return runs[0] if isinstance(runs, list) else runs.iloc[0]
78
-
79
- msg = f"filtered runs has not length of 1.: {len(runs)}"
80
- raise ValueError(msg)
81
-
82
-
83
- def drop_unique_params(runs: DataFrame) -> DataFrame:
84
- def select(column: str) -> bool:
85
- return not column.startswith("params.") or len(runs[column].unique()) > 1
86
-
87
- columns = [select(column) for column in runs.columns]
88
- return runs.iloc[:, columns]
89
-
90
-
91
- def get_param_names(runs: DataFrame) -> list[str]:
92
- def get_name(column: str) -> str:
93
- if column.startswith("params."):
94
- return column.split(".", maxsplit=1)[-1]
95
-
96
- return ""
97
-
98
- columns = [get_name(column) for column in runs.columns]
99
- return [column for column in columns if column]
100
-
101
-
102
- def get_param_dict(runs: DataFrame) -> dict[str, list[str]]:
103
- params = {}
104
- for name in get_param_names(runs):
105
- params[name] = list(runs[f"params.{name}"].unique())
106
-
107
- return params
108
-
109
-
110
- def get_run_id(run: Run | Series | str) -> str:
111
- if isinstance(run, Run):
112
- return run.info.run_id
113
- if isinstance(run, str):
114
- return run
115
- return run.run_id
116
-
117
-
118
- def get_artifact_uri(run: Run | Series | str, artifact_path: str | None = None) -> str:
119
- if isinstance(run, Run):
120
- uri = run.info.artifact_uri
121
- elif isinstance(run, str):
122
- uri = artifact_utils.get_artifact_uri(run_id=run)
123
- else:
124
- uri = run.artifact_uri
125
-
126
- if artifact_path:
127
- uri = f"{uri}/{artifact_path}"
128
-
129
- return uri # type: ignore
130
-
131
-
132
- def get_artifact_dir(run: Run | Series | str) -> Path:
133
- uri = get_artifact_uri(run)
134
- return uri_to_path(uri)
135
-
136
-
137
- def get_artifact_path(
138
- run: Run | Series | str,
139
- artifact_path: str | None = None,
140
- ) -> Path:
141
- artifact_dir = get_artifact_dir(run)
142
- return artifact_dir / artifact_path if artifact_path else artifact_dir
143
-
144
-
145
- def load_config(run: Run | Series | str, output_subdir: str = ".hydra") -> DictConfig:
146
- run_id = get_run_id(run)
147
-
148
- try:
149
- path = mlflow.artifacts.download_artifacts(
150
- run_id=run_id,
151
- artifact_path=f"{output_subdir}/config.yaml",
152
- )
153
- except OSError:
154
- return DictConfig({})
155
-
156
- return OmegaConf.load(path) # type: ignore
157
-
158
-
159
- def get_hydra_output_dir(run: Run | Series | str) -> Path:
160
- path = get_artifact_dir(run) / ".hydra/hydra.yaml"
161
-
162
- if path.exists():
163
- hc = OmegaConf.load(path)
164
- return Path(hc.hydra.runtime.output_dir)
165
-
166
- raise FileNotFoundError
167
-
168
-
169
- def log_hydra_output_dir(run: Run | Series | str) -> None:
170
- output_dir = get_hydra_output_dir(run)
171
- run_id = run if isinstance(run, str) else run.info.run_id
172
- mlflow.log_artifacts(output_dir.as_posix(), run_id=run_id)
@@ -1,29 +0,0 @@
1
- Metadata-Version: 2.3
2
- Name: hydraflow
3
- Version: 0.1.0
4
- Summary: Hydra with MLflow
5
- Project-URL: Source, https://github.com/daizutabi/hydraflow
6
- Project-URL: Issues, https://github.com/daizutabi/hydraflow/issues
7
- Author-email: daizutabi <daizutabi@gmail.com>
8
- License-Expression: MIT
9
- License-File: LICENSE
10
- Classifier: Development Status :: 4 - Beta
11
- Classifier: Programming Language :: Python
12
- Classifier: Programming Language :: Python :: 3.10
13
- Classifier: Programming Language :: Python :: 3.11
14
- Classifier: Programming Language :: Python :: 3.12
15
- Classifier: Topic :: Documentation
16
- Classifier: Topic :: Software Development :: Documentation
17
- Requires-Python: >=3.10
18
- Requires-Dist: hydra-core
19
- Requires-Dist: mlflow
20
- Requires-Dist: watchdog
21
- Provides-Extra: dev
22
- Requires-Dist: pytest-clarity; extra == 'dev'
23
- Requires-Dist: pytest-cov; extra == 'dev'
24
- Requires-Dist: pytest-randomly; extra == 'dev'
25
- Requires-Dist: pytest-xdist; extra == 'dev'
26
- Requires-Dist: setuptools; extra == 'dev'
27
- Description-Content-Type: text/markdown
28
-
29
- # hydraflow
@@ -1,10 +0,0 @@
1
- hydraflow/__init__.py,sha256=9RaHPTloOOJYPUKKfPuK_wxKDr_J9A3rJ_gr-bLABD0,559
2
- hydraflow/config.py,sha256=b3Plh_lmq94loZNw9QP2asd6thCLyTzzYSutH0cONXA,964
3
- hydraflow/context.py,sha256=zBmbZWNLxUF2IDDPregPnR_sh3utmFwFJaneSsBsLDM,2558
4
- hydraflow/mlflow.py,sha256=yDZ_oB1IZdCNNqHm_0LxdZ1Nld28IkW8Xl7NMhWLApE,453
5
- hydraflow/run.py,sha256=XTAD_fd-ivvZ4tbjQLHrf6u5eAGRrrhqvExiZQcFnX8,4591
6
- hydraflow/util.py,sha256=HTymDLqa2UzCw3kNjqHDaAZNdRMnrEAWhCJ7_ZD7ffA,264
7
- hydraflow-0.1.0.dist-info/METADATA,sha256=WuryvAC_8MrC-UerPqbvcWxgBn9ABrnysQ0aRYimw3A,1021
8
- hydraflow-0.1.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
9
- hydraflow-0.1.0.dist-info/licenses/LICENSE,sha256=IGdDrBPqz1O0v_UwCW-NJlbX9Hy9b3uJ11t28y2srmY,1062
10
- hydraflow-0.1.0.dist-info/RECORD,,