hydraflow 0.2.2__py3-none-any.whl → 0.2.3__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
@@ -7,6 +7,7 @@ from .runs import (
7
7
  get_param_dict,
8
8
  get_param_names,
9
9
  get_run,
10
+ list_runs,
10
11
  load_config,
11
12
  search_runs,
12
13
  )
@@ -20,6 +21,7 @@ __all__ = [
20
21
  "get_param_dict",
21
22
  "get_param_names",
22
23
  "get_run",
24
+ "list_runs",
23
25
  "load_config",
24
26
  "log_run",
25
27
  "search_runs",
hydraflow/asyncio.py ADDED
@@ -0,0 +1,199 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ from asyncio.subprocess import PIPE
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ import watchfiles
10
+
11
+ if TYPE_CHECKING:
12
+ from asyncio.streams import StreamReader
13
+ from collections.abc import Callable
14
+
15
+ from watchfiles import Change
16
+
17
+
18
+ # Set up logging
19
+ logging.basicConfig(level=logging.INFO)
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ async def execute_command(
24
+ program: str,
25
+ *args: str,
26
+ stdout: Callable[[str], None] | None = None,
27
+ stderr: Callable[[str], None] | None = None,
28
+ stop_event: asyncio.Event,
29
+ ) -> int:
30
+ """
31
+ Runs a command asynchronously and pass the output to callback functions.
32
+
33
+ Args:
34
+ program (str): The program to run.
35
+ *args (str): Arguments for the program.
36
+ stdout (Callable[[str], None] | None): Callback for standard output.
37
+ stderr (Callable[[str], None] | None): Callback for standard error.
38
+ stop_event (asyncio.Event): Event to signal when the process is done.
39
+
40
+ Returns:
41
+ int: The return code of the process.
42
+ """
43
+ try:
44
+ process = await asyncio.create_subprocess_exec(program, *args, stdout=PIPE, stderr=PIPE)
45
+ await asyncio.gather(
46
+ process_stream(process.stdout, stdout),
47
+ process_stream(process.stderr, stderr),
48
+ )
49
+ returncode = await process.wait()
50
+
51
+ except Exception as e:
52
+ logger.error(f"Error running command: {e}")
53
+ returncode = 1
54
+
55
+ finally:
56
+ stop_event.set()
57
+
58
+ return returncode
59
+
60
+
61
+ async def process_stream(
62
+ stream: StreamReader | None,
63
+ callback: Callable[[str], None] | None,
64
+ ) -> None:
65
+ """
66
+ Reads a stream asynchronously and pass each line to a callback function.
67
+
68
+ Args:
69
+ stream (StreamReader | None): The stream to read from.
70
+ callback (Callable[[str], None] | None): The callback function to handle
71
+ each line.
72
+ """
73
+ if stream is None or callback is None:
74
+ return
75
+
76
+ while True:
77
+ line = await stream.readline()
78
+ if line:
79
+ callback(line.decode().strip())
80
+ else:
81
+ break
82
+
83
+
84
+ async def monitor_file_changes(
85
+ paths: list[str | Path],
86
+ callback: Callable[[set[tuple[Change, str]]], None],
87
+ stop_event: asyncio.Event,
88
+ **awatch_kwargs,
89
+ ) -> None:
90
+ """
91
+ Watches for file changes in specified paths and pass the changes to a
92
+ callback function.
93
+
94
+ Args:
95
+ paths (list[str | Path]): List of paths to monitor for changes.
96
+ callback (Callable[[set[tuple[Change, str]]], None]): The callback
97
+ function to handle file changes.
98
+ stop_event (asyncio.Event): Event to signal when to stop watching.
99
+ **awatch_kwargs: Additional keyword arguments to pass to watchfiles.awatch.
100
+ """
101
+ str_paths = [str(path) for path in paths]
102
+ try:
103
+ async for changes in watchfiles.awatch(*str_paths, stop_event=stop_event, **awatch_kwargs):
104
+ callback(changes)
105
+ except Exception as e:
106
+ logger.error(f"Error watching files: {e}")
107
+
108
+
109
+ async def run_and_monitor(
110
+ program: str,
111
+ *args: str,
112
+ stdout: Callable[[str], None] | None = None,
113
+ stderr: Callable[[str], None] | None = None,
114
+ watch: Callable[[set[tuple[Change, str]]], None] | None = None,
115
+ paths: list[str | Path] | None = None,
116
+ **awatch_kwargs,
117
+ ) -> int:
118
+ """
119
+ Runs a command and optionally watch for file changes concurrently.
120
+
121
+ Args:
122
+ program (str): The program to run.
123
+ *args (str): Arguments for the program.
124
+ stdout (Callable[[str], None] | None): Callback for standard output.
125
+ stderr (Callable[[str], None] | None): Callback for standard error.
126
+ watch (Callable[[set[tuple[Change, str]]], None] | None): Callback for
127
+ file changes.
128
+ paths (list[str | Path] | None): List of paths to monitor for changes.
129
+ """
130
+ stop_event = asyncio.Event()
131
+ run_task = asyncio.create_task(
132
+ execute_command(program, *args, stop_event=stop_event, stdout=stdout, stderr=stderr)
133
+ )
134
+ if watch and paths:
135
+ monitor_task = asyncio.create_task(
136
+ monitor_file_changes(paths, watch, stop_event, **awatch_kwargs)
137
+ )
138
+ else:
139
+ monitor_task = None
140
+
141
+ try:
142
+ if monitor_task:
143
+ await asyncio.gather(run_task, monitor_task)
144
+ else:
145
+ await run_task
146
+
147
+ except Exception as e:
148
+ logger.error(f"Error in run_and_monitor: {e}")
149
+ finally:
150
+ stop_event.set()
151
+ await run_task
152
+ if monitor_task:
153
+ await monitor_task
154
+
155
+ return run_task.result()
156
+
157
+
158
+ def run(
159
+ program: str,
160
+ *args: str,
161
+ stdout: Callable[[str], None] | None = None,
162
+ stderr: Callable[[str], None] | None = None,
163
+ watch: Callable[[set[tuple[Change, str]]], None] | None = None,
164
+ paths: list[str | Path] | None = None,
165
+ **awatch_kwargs,
166
+ ) -> int:
167
+ """
168
+ Run a command synchronously and optionally watch for file changes.
169
+
170
+ This function is a synchronous wrapper around the asynchronous `run_and_monitor` function.
171
+ It runs a specified command and optionally monitors specified paths for file changes,
172
+ invoking the provided callbacks for standard output, standard error, and file changes.
173
+
174
+ Args:
175
+ program (str): The program to run.
176
+ *args (str): Arguments for the program.
177
+ stdout (Callable[[str], None] | None): Callback for handling standard output lines.
178
+ stderr (Callable[[str], None] | None): Callback for handling standard error lines.
179
+ watch (Callable[[set[tuple[Change, str]]], None] | None): Callback for handling file changes.
180
+ paths (list[str | Path] | None): List of paths to monitor for file changes.
181
+ **awatch_kwargs: Additional keyword arguments to pass to `watchfiles.awatch`.
182
+
183
+ Returns:
184
+ int: The return code of the process.
185
+ """
186
+ if watch and not paths:
187
+ paths = [Path.cwd()]
188
+
189
+ return asyncio.run(
190
+ run_and_monitor(
191
+ program,
192
+ *args,
193
+ stdout=stdout,
194
+ stderr=stderr,
195
+ watch=watch,
196
+ paths=paths,
197
+ **awatch_kwargs,
198
+ )
199
+ )
hydraflow/runs.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
- This module provides functionality for managing and interacting with MLflow runs.
3
- It includes the `RunCollection` class and various methods to filter runs,
4
- retrieve run information, log artifacts, and load configurations.
2
+ This module provides functionality for managing and interacting with MLflow
3
+ runs. It includes the `RunCollection` class and various methods to filter
4
+ runs, retrieve run information, log artifacts, and load configurations.
5
5
  """
6
6
 
7
7
  from __future__ import annotations
@@ -37,34 +37,39 @@ def search_runs(
37
37
  """
38
38
  Search for Runs that fit the specified criteria.
39
39
 
40
- This function wraps the `mlflow.search_runs` function and returns the results
41
- as a `RunCollection` object. It allows for flexible searching of MLflow runs based on
42
- various criteria.
40
+ This function wraps the `mlflow.search_runs` function and returns the
41
+ results as a `RunCollection` object. It allows for flexible searching of
42
+ MLflow runs based on various criteria.
43
43
 
44
44
  Note:
45
45
  The returned runs are sorted by their start time in ascending order.
46
46
 
47
47
  Args:
48
- experiment_ids: List of experiment IDs. Search can work with experiment IDs or
49
- experiment names, but not both in the same call. Values other than
50
- ``None`` or ``[]`` will result in error if ``experiment_names`` is
51
- also not ``None`` or ``[]``. ``None`` will default to the active
52
- experiment if ``experiment_names`` is ``None`` or ``[]``.
48
+ experiment_ids: List of experiment IDs. Search can work with experiment
49
+ IDs or experiment names, but not both in the same call. Values
50
+ other than ``None`` or ``[]`` will result in error if
51
+ ``experiment_names`` is also not ``None`` or ``[]``. ``None`` will
52
+ default to the active experiment if ``experiment_names`` is ``None``
53
+ or ``[]``.
53
54
  filter_string: Filter query string, defaults to searching all runs.
54
- run_view_type: one of enum values ``ACTIVE_ONLY``, ``DELETED_ONLY``, or ``ALL`` runs
55
- defined in :py:class:`mlflow.entities.ViewType`.
56
- max_results: The maximum number of runs to put in the dataframe. Default is 100,000
57
- to avoid causing out-of-memory issues on the user's machine.
58
- order_by: List of columns to order by (e.g., "metrics.rmse"). The ``order_by`` column
59
- can contain an optional ``DESC`` or ``ASC`` value. The default is ``ASC``.
60
- The default ordering is to sort by ``start_time DESC``, then ``run_id``.
61
- search_all_experiments: Boolean specifying whether all experiments should be searched.
62
- Only honored if ``experiment_ids`` is ``[]`` or ``None``.
63
- experiment_names: List of experiment names. Search can work with experiment IDs or
64
- experiment names, but not both in the same call. Values other
65
- than ``None`` or ``[]`` will result in error if ``experiment_ids``
66
- is also not ``None`` or ``[]``. ``None`` will default to the active
67
- experiment if ``experiment_ids`` is ``None`` or ``[]``.
55
+ run_view_type: one of enum values ``ACTIVE_ONLY``, ``DELETED_ONLY``, or
56
+ ``ALL`` runs defined in :py:class:`mlflow.entities.ViewType`.
57
+ max_results: The maximum number of runs to put in the dataframe. Default
58
+ is 100,000 to avoid causing out-of-memory issues on the user's
59
+ machine.
60
+ order_by: List of columns to order by (e.g., "metrics.rmse"). The
61
+ ``order_by`` column can contain an optional ``DESC`` or ``ASC``
62
+ value. The default is ``ASC``. The default ordering is to sort by
63
+ ``start_time DESC``, then ``run_id``.
64
+ search_all_experiments: Boolean specifying whether all experiments
65
+ should be searched. Only honored if ``experiment_ids`` is ``[]`` or
66
+ ``None``.
67
+ experiment_names: List of experiment names. Search can work with
68
+ experiment IDs or experiment names, but not both in the same call.
69
+ Values other than ``None`` or ``[]`` will result in error if
70
+ ``experiment_ids`` is also not ``None`` or ``[]``. ``None`` will
71
+ default to the active experiment if ``experiment_ids`` is ``None``
72
+ or ``[]``.
68
73
 
69
74
  Returns:
70
75
  A `RunCollection` object containing the search results.
@@ -133,27 +138,77 @@ class RunCollection:
133
138
  def __len__(self) -> int:
134
139
  return len(self._runs)
135
140
 
141
+ def first(self) -> Run:
142
+ """
143
+ Get the first run in the collection.
144
+
145
+ Returns:
146
+ The first run object in the collection.
147
+
148
+ Raises:
149
+ ValueError: If the collection is empty.
150
+ """
151
+ if not self._runs:
152
+ raise ValueError("The collection is empty.")
153
+
154
+ return self._runs[0]
155
+
156
+ def try_first(self) -> Run | None:
157
+ """
158
+ Try to get the first run in the collection.
159
+
160
+ Returns:
161
+ The first run object in the collection, or None if the collection
162
+ is empty.
163
+ """
164
+ return self._runs[0] if self._runs else None
165
+
166
+ def last(self) -> Run:
167
+ """
168
+ Get the last run in the collection.
169
+
170
+ Returns:
171
+ The last run object in the collection.
172
+
173
+ Raises:
174
+ ValueError: If the collection is empty.
175
+ """
176
+ if not self._runs:
177
+ raise ValueError("The collection is empty.")
178
+
179
+ return self._runs[-1]
180
+
181
+ def try_last(self) -> Run | None:
182
+ """
183
+ Try to get the last run in the collection.
184
+
185
+ Returns:
186
+ The last run object in the collection, or None if the collection is
187
+ empty.
188
+ """
189
+ return self._runs[-1] if self._runs else None
190
+
136
191
  def filter(self, config: object | None = None, **kwargs) -> RunCollection:
137
192
  """
138
193
  Filter the runs based on the provided configuration.
139
194
 
140
195
  This method filters the runs in the collection according to the
141
- specified configuration object and additional key-value pairs.
142
- The configuration object and key-value pairs should contain
143
- key-value pairs that correspond to the parameters of the runs.
144
- Only the runs that match all the specified parameters will be
145
- included in the returned `RunCollection` object.
196
+ specified configuration object and additional key-value pairs. The
197
+ configuration object and key-value pairs should contain key-value pairs
198
+ that correspond to the parameters of the runs. Only the runs that match
199
+ all the specified parameters will be included in the returned
200
+ `RunCollection` object.
146
201
 
147
202
  The filtering supports:
148
203
  - Exact matches for single values.
149
204
  - Membership checks for lists of values.
150
- - Range checks for tuples of two values (inclusive of the lower bound and
151
- exclusive of the upper bound).
205
+ - Range checks for tuples of two values (inclusive of the lower bound
206
+ and exclusive of the upper bound).
152
207
 
153
208
  Args:
154
- config: The configuration object to filter the runs. This can be any
155
- object that provides key-value pairs through the `iter_params`
156
- function.
209
+ config: The configuration object to filter the runs. This can be
210
+ any object that provides key-value pairs through the
211
+ `iter_params` function.
157
212
  **kwargs: Additional key-value pairs to filter the runs.
158
213
 
159
214
  Returns:
@@ -161,80 +216,161 @@ class RunCollection:
161
216
  """
162
217
  return RunCollection(filter_runs(self._runs, config, **kwargs))
163
218
 
164
- def find(self, config: object | None = None, **kwargs) -> Run | None:
219
+ def find(self, config: object | None = None, **kwargs) -> Run:
165
220
  """
166
221
  Find the first run based on the provided configuration.
167
222
 
168
223
  This method filters the runs in the collection according to the
169
224
  specified configuration object and returns the first run that matches
170
- the provided parameters. If no run matches the criteria, None is returned.
225
+ the provided parameters. If no run matches the criteria, a `ValueError`
226
+ is raised.
171
227
 
172
228
  Args:
173
229
  config: The configuration object to identify the run.
174
230
  **kwargs: Additional key-value pairs to filter the runs.
175
231
 
176
232
  Returns:
177
- The first run object that matches the provided configuration, or None
178
- if no runs match the criteria.
233
+ The first run object that matches the provided configuration.
234
+
235
+ Raises:
236
+ ValueError: If no run matches the criteria.
179
237
 
180
238
  See Also:
181
- RunCollection.filter: The method that performs the actual filtering logic.
239
+ RunCollection.filter: The method that performs the actual filtering
240
+ logic.
182
241
  """
183
242
  return find_run(self._runs, config, **kwargs)
184
243
 
185
- def find_last(self, config: object | None = None, **kwargs) -> Run | None:
244
+ def try_find(self, config: object | None = None, **kwargs) -> Run | None:
245
+ """
246
+ Find the first run based on the provided configuration.
247
+
248
+ This method filters the runs in the collection according to the
249
+ specified configuration object and returns the first run that matches
250
+ the provided parameters. If no run matches the criteria, None is
251
+ returned.
252
+
253
+ Args:
254
+ config: The configuration object to identify the run.
255
+ **kwargs: Additional key-value pairs to filter the runs.
256
+
257
+ Returns:
258
+ The first run object that matches the provided configuration, or
259
+ None if no runs match the criteria.
260
+
261
+ See Also:
262
+ RunCollection.filter: The method that performs the actual filtering
263
+ logic.
264
+ """
265
+ return try_find_run(self._runs, config, **kwargs)
266
+
267
+ def find_last(self, config: object | None = None, **kwargs) -> Run:
186
268
  """
187
269
  Find the last run based on the provided configuration.
188
270
 
189
271
  This method filters the runs in the collection according to the
190
272
  specified configuration object and returns the last run that matches
191
- the provided parameters. If no run matches the criteria, None is returned.
273
+ the provided parameters. If no run matches the criteria, a `ValueError`
274
+ is raised.
192
275
 
193
276
  Args:
194
277
  config: The configuration object to identify the run.
195
278
  **kwargs: Additional key-value pairs to filter the runs.
196
279
 
197
280
  Returns:
198
- The last run object that matches the provided configuration, or None
199
- if no runs match the criteria.
281
+ The last run object that matches the provided configuration.
282
+
283
+ Raises:
284
+ ValueError: If no run matches the criteria.
200
285
 
201
286
  See Also:
202
- RunCollection.filter: The method that performs the actual filtering logic.
287
+ RunCollection.filter: The method that performs the actual filtering
288
+ logic.
203
289
  """
204
290
  return find_last_run(self._runs, config, **kwargs)
205
291
 
206
- def get(self, config: object | None = None, **kwargs) -> Run | None:
292
+ def try_find_last(self, config: object | None = None, **kwargs) -> Run | None:
293
+ """
294
+ Find the last run based on the provided configuration.
295
+
296
+ This method filters the runs in the collection according to the
297
+ specified configuration object and returns the last run that matches
298
+ the provided parameters. If no run matches the criteria, None is
299
+ returned.
300
+
301
+ Args:
302
+ config: The configuration object to identify the run.
303
+ **kwargs: Additional key-value pairs to filter the runs.
304
+
305
+ Returns:
306
+ The last run object that matches the provided configuration, or
307
+ None if no runs match the criteria.
308
+
309
+ See Also:
310
+ RunCollection.filter: The method that performs the actual filtering
311
+ logic.
312
+ """
313
+ return try_find_last_run(self._runs, config, **kwargs)
314
+
315
+ def get(self, config: object | None = None, **kwargs) -> Run:
207
316
  """
208
317
  Retrieve a specific run based on the provided configuration.
209
318
 
210
319
  This method filters the runs in the collection according to the
211
- specified configuration object and returns the run that matches
212
- the provided parameters. If more than one run matches the criteria,
213
- a `ValueError` is raised.
320
+ specified configuration object and returns the run that matches the
321
+ provided parameters. If no run matches the criteria, or if more than
322
+ one run matches the criteria, a `ValueError` is raised.
214
323
 
215
324
  Args:
216
325
  config: The configuration object to identify the run.
217
326
  **kwargs: Additional key-value pairs to filter the runs.
218
327
 
219
328
  Returns:
220
- The run object that matches the provided configuration, or None
221
- if no runs match the criteria.
329
+ The run object that matches the provided configuration.
222
330
 
223
331
  Raises:
224
- ValueError: If more than one run matches the criteria.
332
+ ValueError: If no run matches the criteria or if more than one run
333
+ matches the criteria.
225
334
 
226
335
  See Also:
227
- RunCollection.filter: The method that performs the actual filtering logic.
336
+ RunCollection.filter: The method that performs the actual filtering
337
+ logic.
228
338
  """
229
339
  return get_run(self._runs, config, **kwargs)
230
340
 
341
+ def try_get(self, config: object | None = None, **kwargs) -> Run | None:
342
+ """
343
+ Retrieve a specific run based on the provided configuration.
344
+
345
+ This method filters the runs in the collection according to the
346
+ specified configuration object and returns the run that matches the
347
+ provided parameters. If no run matches the criteria, None is returned.
348
+ If more than one run matches the criteria, a `ValueError` is raised.
349
+
350
+ Args:
351
+ config: The configuration object to identify the run.
352
+ **kwargs: Additional key-value pairs to filter the runs.
353
+
354
+ Returns:
355
+ The run object that matches the provided configuration, or None if
356
+ no runs match the criteria.
357
+
358
+ Raises:
359
+ ValueError: If more than one run matches the criteria.
360
+
361
+ See Also:
362
+ RunCollection.filter: The method that performs the actual filtering
363
+ logic.
364
+ """
365
+ return try_get_run(self._runs, config, **kwargs)
366
+
231
367
  def get_param_names(self) -> list[str]:
232
368
  """
233
369
  Get the parameter names from the runs.
234
370
 
235
- This method extracts the unique parameter names from the provided list of runs.
236
- It iterates through each run and collects the parameter names into a set to
237
- ensure uniqueness.
371
+ This method extracts the unique parameter names from the provided list
372
+ of runs. It iterates through each run and collects the parameter names
373
+ into a set to ensure uniqueness.
238
374
 
239
375
  Returns:
240
376
  A list of unique parameter names.
@@ -246,101 +382,93 @@ class RunCollection:
246
382
  Get the parameter dictionary from the list of runs.
247
383
 
248
384
  This method extracts the parameter names and their corresponding values
249
- from the provided list of runs. It iterates through each run and collects
250
- the parameter values into a dictionary where the keys are parameter names
251
- and the values are lists of parameter values.
385
+ from the provided list of runs. It iterates through each run and
386
+ collects the parameter values into a dictionary where the keys are
387
+ parameter names and the values are lists of parameter values.
252
388
 
253
389
  Returns:
254
- A dictionary where the keys are parameter names and the values are lists
255
- of parameter values.
390
+ A dictionary where the keys are parameter names and the values are
391
+ lists of parameter values.
256
392
  """
257
393
  return get_param_dict(self._runs)
258
394
 
259
- def first(self) -> Run | None:
260
- """
261
- Return the first run in the collection.
262
-
263
- Returns:
264
- The first Run object if the collection is not empty, otherwise None.
265
- """
266
- return self._runs[0] if self._runs else None
267
-
268
- def last(self) -> Run | None:
269
- """
270
- Return the last run in the collection.
271
-
272
- Returns:
273
- The last Run object if the collection is not empty, otherwise None.
274
- """
275
- return self._runs[-1] if self._runs else None
276
-
277
395
  def map(self, func: Callable[[Run], T]) -> Iterator[T]:
278
396
  """
279
- Apply a function to each run in the collection and return an iterator of results.
397
+ Apply a function to each run in the collection and return an iterator of
398
+ results.
280
399
 
281
400
  Args:
282
- func: A function that takes a Run object and returns a result.
401
+ func: A function that takes a run and returns a result.
283
402
 
284
- Returns:
285
- An iterator of results obtained by applying the function to each run
286
- in the collection.
403
+ Yields:
404
+ Results obtained by applying the function to each run in the
405
+ collection.
287
406
  """
288
407
  return (func(run) for run in self._runs)
289
408
 
290
409
  def map_run_id(self, func: Callable[[str], T]) -> Iterator[T]:
291
410
  """
292
- Apply a function to each run id in the collection and return an iterator of results.
411
+ Apply a function to each run id in the collection and return an iterator
412
+ of results.
293
413
 
294
414
  Args:
295
415
  func: A function that takes a run id and returns a result.
296
416
 
297
- Returns:
298
- An iterator of results obtained by applying the function to each run id
299
- in the collection.
417
+ Yields:
418
+ Results obtained by applying the function to each run id in the
419
+ collection.
300
420
  """
301
421
  return (func(run.info.run_id) for run in self._runs)
302
422
 
303
423
  def map_config(self, func: Callable[[DictConfig], T]) -> Iterator[T]:
304
424
  """
305
- Apply a function to each run config in the collection and return an iterator of results.
425
+ Apply a function to each run configuration in the collection and return
426
+ an iterator of results.
306
427
 
307
428
  Args:
308
- func: A function that takes a run config and returns a result.
429
+ func: A function that takes a run configuration and returns a
430
+ result.
309
431
 
310
- Returns:
311
- An iterator of results obtained by applying the function to each run config
432
+ Yields:
433
+ Results obtained by applying the function to each run configuration
312
434
  in the collection.
313
435
  """
314
436
  return (func(load_config(run)) for run in self._runs)
315
437
 
316
438
  def map_uri(self, func: Callable[[str | None], T]) -> Iterator[T]:
317
439
  """
318
- Apply a function to each artifact URI in the collection and return an iterator of results.
440
+ Apply a function to each artifact URI in the collection and return an
441
+ iterator of results.
319
442
 
320
- This method iterates over each run in the collection, retrieves the artifact URI,
321
- and applies the provided function to it. If a run does not have an artifact URI,
322
- None is passed to the function.
443
+ This method iterates over each run in the collection, retrieves the
444
+ artifact URI, and applies the provided function to it. If a run does not
445
+ have an artifact URI, None is passed to the function.
323
446
 
324
447
  Args:
325
- func: A function that takes an artifact URI (string or None) and returns a result.
448
+ func: A function that takes an artifact URI (string or None) and
449
+ returns a result.
326
450
 
327
451
  Yields:
328
- The results obtained by applying the function to each artifact URI in the collection.
452
+ Results obtained by applying the function to each artifact URI in the
453
+ collection.
329
454
  """
330
455
  return (func(run.info.artifact_uri) for run in self._runs)
331
456
 
332
457
  def map_dir(self, func: Callable[[str], T]) -> Iterator[T]:
333
458
  """
334
- Apply a function to each artifact directory in the collection and return an iterator of results.
459
+ Apply a function to each artifact directory in the collection and return
460
+ an iterator of results.
335
461
 
336
- This method iterates over each run in the collection, downloads the artifact directory,
337
- and applies the provided function to the directory path.
462
+ This method iterates over each run in the collection, downloads the
463
+ artifact directory, and applies the provided function to the directory
464
+ path.
338
465
 
339
466
  Args:
340
- func: A function that takes an artifact directory path (string) and returns a result.
467
+ func: A function that takes an artifact directory path (string) and
468
+ returns a result.
341
469
 
342
- Returns:
343
- An iterator of results obtained by applying the function to each artifact directory
470
+ Yields:
471
+ Results obtained by applying the function to each artifact directory
344
472
  in the collection.
345
473
  """
346
474
  return (func(download_artifacts(run_id=run.info.run_id)) for run in self._runs)
@@ -350,8 +478,9 @@ def _param_matches(run: Run, key: str, value: Any) -> bool:
350
478
  """
351
479
  Check if the run's parameter matches the specified key-value pair.
352
480
 
353
- This function checks if the run's parameters contain the specified key-value pair.
354
- It handles different types of values, including lists and tuples.
481
+ This function checks if the run's parameters contain the specified
482
+ key-value pair. It handles different types of values, including lists
483
+ and tuples.
355
484
 
356
485
  Args:
357
486
  run: The run object to check.
@@ -359,16 +488,11 @@ def _param_matches(run: Run, key: str, value: Any) -> bool:
359
488
  value: The parameter value to check.
360
489
 
361
490
  Returns:
362
- True if the run's parameter matches the specified key-value pair, False otherwise.
491
+ True if the run's parameter matches the specified key-value pair,
492
+ False otherwise.
363
493
  """
364
494
  param = run.data.params.get(key, value)
365
495
 
366
- # FIXME: This is a workaround to handle the case where the parameter value is a list
367
- # We need to improve the logic to handle different types of values
368
- # For now, we assume that if the parameter is a list, we should check if it contains the value
369
- # This is not ideal, but it works for the case where the parameter value is a list of strings
370
- # We should improve the logic to handle different types of values in the future
371
-
372
496
  if param is None:
373
497
  return False
374
498
 
@@ -392,8 +516,8 @@ def filter_runs(runs: list[Run], config: object | None = None, **kwargs) -> list
392
516
  specified configuration object and additional key-value pairs.
393
517
  The configuration object and key-value pairs should contain
394
518
  key-value pairs that correspond to the parameters of the runs.
395
- Only the runs that match all the specified parameters will be
396
- included in the returned list of runs.
519
+ Only the runs that match all the specified parameters will
520
+ be included in the returned list of runs.
397
521
 
398
522
  The filtering supports:
399
523
  - Exact matches for single values.
@@ -403,8 +527,9 @@ def filter_runs(runs: list[Run], config: object | None = None, **kwargs) -> list
403
527
 
404
528
  Args:
405
529
  runs: The list of runs to filter.
406
- config: The configuration object to filter the runs. This can be any object that
407
- provides key-value pairs through the `iter_params` function.
530
+ config: The configuration object to filter the runs. This can be any
531
+ object that provides key-value pairs through the `iter_params`
532
+ function.
408
533
  **kwargs: Additional key-value pairs to filter the runs.
409
534
 
410
535
  Returns:
@@ -419,7 +544,38 @@ def filter_runs(runs: list[Run], config: object | None = None, **kwargs) -> list
419
544
  return runs
420
545
 
421
546
 
422
- def find_run(runs: list[Run], config: object | None = None, **kwargs) -> Run | None:
547
+ def find_run(runs: list[Run], config: object | None = None, **kwargs) -> Run:
548
+ """
549
+ Find the first run based on the provided configuration.
550
+
551
+ This method filters the runs in the collection according to the
552
+ specified configuration object and returns the first run that matches
553
+ the provided parameters. If no run matches the criteria, a `ValueError` is
554
+ raised.
555
+
556
+ Args:
557
+ runs: The runs to filter.
558
+ config: The configuration object to identify the run.
559
+ **kwargs: Additional key-value pairs to filter the runs.
560
+
561
+ Returns:
562
+ The first run object that matches the provided configuration.
563
+
564
+ Raises:
565
+ ValueError: If no run matches the criteria.
566
+
567
+ See Also:
568
+ RunCollection.filter: The method that performs the actual filtering logic.
569
+ """
570
+ filtered_runs = filter_runs(runs, config, **kwargs)
571
+
572
+ if len(filtered_runs) == 0:
573
+ raise ValueError("No run matches the provided configuration.")
574
+
575
+ return filtered_runs[0]
576
+
577
+
578
+ def try_find_run(runs: list[Run], config: object | None = None, **kwargs) -> Run | None:
423
579
  """
424
580
  Find the first run based on the provided configuration.
425
581
 
@@ -436,11 +592,47 @@ def find_run(runs: list[Run], config: object | None = None, **kwargs) -> Run | N
436
592
  The first run object that matches the provided configuration, or None
437
593
  if no runs match the criteria.
438
594
  """
439
- runs = filter_runs(runs, config, **kwargs)
440
- return runs[0] if runs else None
595
+ filtered_runs = filter_runs(runs, config, **kwargs)
596
+
597
+ if len(filtered_runs) == 0:
598
+ return None
599
+
600
+ return filtered_runs[0]
441
601
 
442
602
 
443
- def find_last_run(runs: list[Run], config: object | None = None, **kwargs) -> Run | None:
603
+ def find_last_run(runs: list[Run], config: object | None = None, **kwargs) -> Run:
604
+ """
605
+ Find the last run based on the provided configuration.
606
+
607
+ This method filters the runs in the collection according to the
608
+ specified configuration object and returns the last run that matches
609
+ the provided parameters. If no run matches the criteria, a `ValueError`
610
+ is raised.
611
+
612
+ Args:
613
+ runs: The runs to filter.
614
+ config: The configuration object to identify the run.
615
+ **kwargs: Additional key-value pairs to filter the runs.
616
+
617
+ Returns:
618
+ The last run object that matches the provided configuration.
619
+
620
+ Raises:
621
+ ValueError: If no run matches the criteria.
622
+
623
+ See Also:
624
+ RunCollection.filter: The method that performs the actual filtering
625
+ logic.
626
+ """
627
+ filtered_runs = filter_runs(runs, config, **kwargs)
628
+
629
+ if len(filtered_runs) == 0:
630
+ raise ValueError("No run matches the provided configuration.")
631
+
632
+ return filtered_runs[-1]
633
+
634
+
635
+ def try_find_last_run(runs: list[Run], config: object | None = None, **kwargs) -> Run | None:
444
636
  """
445
637
  Find the last run based on the provided configuration.
446
638
 
@@ -457,18 +649,62 @@ def find_last_run(runs: list[Run], config: object | None = None, **kwargs) -> Ru
457
649
  The last run object that matches the provided configuration, or None
458
650
  if no runs match the criteria.
459
651
  """
460
- runs = filter_runs(runs, config, **kwargs)
461
- return runs[-1] if runs else None
652
+ filtered_runs = filter_runs(runs, config, **kwargs)
653
+
654
+ if len(filtered_runs) == 0:
655
+ return None
656
+
657
+ return filtered_runs[-1]
658
+
659
+
660
+ def get_run(runs: list[Run], config: object | None = None, **kwargs) -> Run:
661
+ """
662
+ Retrieve a specific run based on the provided configuration.
663
+
664
+ This method filters the runs in the collection according to the
665
+ specified configuration object and returns the run that matches
666
+ the provided parameters. If no run matches the criteria, or if more
667
+ than one run matches the criteria, a `ValueError` is raised.
668
+
669
+ Args:
670
+ runs: The runs to filter.
671
+ config: The configuration object to identify the run.
672
+ **kwargs: Additional key-value pairs to filter the runs.
673
+
674
+ Returns:
675
+ The run object that matches the provided configuration.
676
+
677
+ Raises:
678
+ ValueError: If no run matches the criteria or if more than one run
679
+ matches the criteria.
680
+
681
+ See Also:
682
+ RunCollection.filter: The method that performs the actual filtering
683
+ logic.
684
+ """
685
+ filtered_runs = filter_runs(runs, config, **kwargs)
462
686
 
687
+ if len(filtered_runs) == 0:
688
+ raise ValueError("No run matches the provided configuration.")
463
689
 
464
- def get_run(runs: list[Run], config: object | None = None, **kwargs) -> Run | None:
690
+ if len(filtered_runs) == 1:
691
+ return filtered_runs[0]
692
+
693
+ msg = (
694
+ f"Multiple runs were filtered. Expected number of runs is 1, "
695
+ f"but found {len(filtered_runs)} runs."
696
+ )
697
+ raise ValueError(msg)
698
+
699
+
700
+ def try_get_run(runs: list[Run], config: object | None = None, **kwargs) -> Run | None:
465
701
  """
466
702
  Retrieve a specific run based on the provided configuration.
467
703
 
468
704
  This method filters the runs in the collection according to the
469
705
  specified configuration object and returns the run that matches
470
- the provided parameters. If more than one run matches the criteria,
471
- a `ValueError` is raised.
706
+ the provided parameters. If no run matches the criteria, None is returned.
707
+ If more than one run matches the criteria, a `ValueError` is raised.
472
708
 
473
709
  Args:
474
710
  runs: The runs to filter.
@@ -481,16 +717,23 @@ def get_run(runs: list[Run], config: object | None = None, **kwargs) -> Run | No
481
717
 
482
718
  Raises:
483
719
  ValueError: If more than one run matches the criteria.
720
+
721
+ See Also:
722
+ RunCollection.filter: The method that performs the actual filtering
723
+ logic.
484
724
  """
485
- runs = filter_runs(runs, config, **kwargs)
725
+ filtered_runs = filter_runs(runs, config, **kwargs)
486
726
 
487
- if len(runs) == 0:
727
+ if len(filtered_runs) == 0:
488
728
  return None
489
729
 
490
- if len(runs) == 1:
491
- return runs[0]
730
+ if len(filtered_runs) == 1:
731
+ return filtered_runs[0]
492
732
 
493
- msg = f"Multiple runs were filtered. Expected number of runs is 1, but found {len(runs)} runs."
733
+ msg = (
734
+ "Multiple runs were filtered. Expected number of runs is 1, "
735
+ f"but found {len(filtered_runs)} runs."
736
+ )
494
737
  raise ValueError(msg)
495
738
 
496
739
 
@@ -498,9 +741,9 @@ def get_param_names(runs: list[Run]) -> list[str]:
498
741
  """
499
742
  Get the parameter names from the runs.
500
743
 
501
- This method extracts the unique parameter names from the provided list of runs.
502
- It iterates through each run and collects the parameter names into a set to
503
- ensure uniqueness.
744
+ This method extracts the unique parameter names from the provided list of
745
+ runs. It iterates through each run and collects the parameter names into a
746
+ set to ensure uniqueness.
504
747
 
505
748
  Args:
506
749
  runs: The list of runs from which to extract parameter names.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: hydraflow
3
- Version: 0.2.2
3
+ Version: 0.2.3
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
@@ -20,7 +20,9 @@ Requires-Dist: hydra-core>1.3
20
20
  Requires-Dist: mlflow>2.15
21
21
  Requires-Dist: setuptools
22
22
  Requires-Dist: watchdog
23
+ Requires-Dist: watchfiles
23
24
  Provides-Extra: dev
25
+ Requires-Dist: pytest-asyncio; extra == 'dev'
24
26
  Requires-Dist: pytest-clarity; extra == 'dev'
25
27
  Requires-Dist: pytest-cov; extra == 'dev'
26
28
  Requires-Dist: pytest-randomly; extra == 'dev'
@@ -0,0 +1,10 @@
1
+ hydraflow/__init__.py,sha256=9v7p2ezUd_LMoRJQS0ay8c7fpaqPZ6Ofq7YPT0rSO5I,528
2
+ hydraflow/asyncio.py,sha256=yh851L315QHzRBwq6r-uwO2oZKgz1JawHp-fswfxT1E,6175
3
+ hydraflow/config.py,sha256=FNTuCppjCMrZKVByJMrWKbgj3HeMWWwAmQNoyFe029Y,2087
4
+ hydraflow/context.py,sha256=MqkEhKEZL_N3eb3v5u9D4EqKkiSmiPyXXafhPkALRlg,5129
5
+ hydraflow/mlflow.py,sha256=_Los9E38eG8sTiN8bGwZmvjCrS0S-wSGiA4fyhQM3Zw,2251
6
+ hydraflow/runs.py,sha256=0BXSBbNkELP3CzaCGBkejOkpyk5uQUxrdknJPRwR400,29022
7
+ hydraflow-0.2.3.dist-info/METADATA,sha256=h5Pxy6EnxTlyyGL8NRr14ZHtLhA9ldmM9GP5sES6KWU,4304
8
+ hydraflow-0.2.3.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
9
+ hydraflow-0.2.3.dist-info/licenses/LICENSE,sha256=IGdDrBPqz1O0v_UwCW-NJlbX9Hy9b3uJ11t28y2srmY,1062
10
+ hydraflow-0.2.3.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- hydraflow/__init__.py,sha256=ht4I3q_Ronw2jzk_QRsV-IzObR31F_4Wy7Ve0syNm-8,496
2
- hydraflow/config.py,sha256=FNTuCppjCMrZKVByJMrWKbgj3HeMWWwAmQNoyFe029Y,2087
3
- hydraflow/context.py,sha256=MqkEhKEZL_N3eb3v5u9D4EqKkiSmiPyXXafhPkALRlg,5129
4
- hydraflow/mlflow.py,sha256=_Los9E38eG8sTiN8bGwZmvjCrS0S-wSGiA4fyhQM3Zw,2251
5
- hydraflow/runs.py,sha256=kO7Gl9CeS2HjB0y_emGXNMRJTxNoqXBEJ7Ggq96nhMg,22050
6
- hydraflow-0.2.2.dist-info/METADATA,sha256=C2lfD6jTDdHyexxATZWfdRQHAUgSOHx7IgvmBUj4tTQ,4232
7
- hydraflow-0.2.2.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
8
- hydraflow-0.2.2.dist-info/licenses/LICENSE,sha256=IGdDrBPqz1O0v_UwCW-NJlbX9Hy9b3uJ11t28y2srmY,1062
9
- hydraflow-0.2.2.dist-info/RECORD,,