FlowerPower 0.30.0__py3-none-any.whl → 0.31.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. flowerpower/cfg/__init__.py +143 -25
  2. flowerpower/cfg/base.py +132 -11
  3. flowerpower/cfg/exceptions.py +53 -0
  4. flowerpower/cfg/pipeline/__init__.py +151 -35
  5. flowerpower/cfg/pipeline/adapter.py +1 -0
  6. flowerpower/cfg/pipeline/builder.py +24 -25
  7. flowerpower/cfg/pipeline/builder_adapter.py +142 -0
  8. flowerpower/cfg/pipeline/builder_executor.py +101 -0
  9. flowerpower/cfg/pipeline/run.py +99 -40
  10. flowerpower/cfg/project/__init__.py +59 -14
  11. flowerpower/cfg/project/adapter.py +6 -0
  12. flowerpower/cli/__init__.py +8 -2
  13. flowerpower/cli/cfg.py +0 -38
  14. flowerpower/cli/pipeline.py +121 -83
  15. flowerpower/cli/utils.py +120 -71
  16. flowerpower/flowerpower.py +94 -120
  17. flowerpower/pipeline/config_manager.py +180 -0
  18. flowerpower/pipeline/executor.py +126 -0
  19. flowerpower/pipeline/lifecycle_manager.py +231 -0
  20. flowerpower/pipeline/manager.py +121 -274
  21. flowerpower/pipeline/pipeline.py +66 -278
  22. flowerpower/pipeline/registry.py +45 -4
  23. flowerpower/utils/__init__.py +19 -0
  24. flowerpower/utils/adapter.py +286 -0
  25. flowerpower/utils/callback.py +73 -67
  26. flowerpower/utils/config.py +306 -0
  27. flowerpower/utils/executor.py +178 -0
  28. flowerpower/utils/filesystem.py +194 -0
  29. flowerpower/utils/misc.py +312 -138
  30. flowerpower/utils/security.py +221 -0
  31. {flowerpower-0.30.0.dist-info → flowerpower-0.31.1.dist-info}/METADATA +2 -2
  32. flowerpower-0.31.1.dist-info/RECORD +53 -0
  33. flowerpower/cfg/pipeline/_schedule.py +0 -32
  34. flowerpower-0.30.0.dist-info/RECORD +0 -42
  35. {flowerpower-0.30.0.dist-info → flowerpower-0.31.1.dist-info}/WHEEL +0 -0
  36. {flowerpower-0.30.0.dist-info → flowerpower-0.31.1.dist-info}/entry_points.txt +0 -0
  37. {flowerpower-0.30.0.dist-info → flowerpower-0.31.1.dist-info}/licenses/LICENSE +0 -0
  38. {flowerpower-0.30.0.dist-info → flowerpower-0.31.1.dist-info}/top_level.txt +0 -0
flowerpower/utils/misc.py CHANGED
@@ -8,137 +8,220 @@ from typing import Any
8
8
 
9
9
  import msgspec
10
10
  from fsspec_utils import AbstractFileSystem, filesystem
11
-
12
- if importlib.util.find_spec("joblib"):
13
- from joblib import Parallel, delayed
14
- from rich.progress import (BarColumn, Progress, TextColumn,
15
- TimeElapsedColumn)
16
-
17
- def run_parallel(
18
- func: callable,
19
- *args,
20
- n_jobs: int = -1,
21
- backend: str = "threading",
22
- verbose: bool = True,
23
- **kwargs,
24
- ) -> list[any]:
25
- """Runs a function for a list of parameters in parallel.
26
-
27
- Args:
28
- func (Callable): function to run in parallel
29
- *args: Positional arguments. Can be single values or iterables
30
- n_jobs (int, optional): Number of joblib workers. Defaults to -1
31
- backend (str, optional): joblib backend. Valid options are
32
- `loky`,`threading`, `mutliprocessing` or `sequential`. Defaults to "threading"
33
- verbose (bool, optional): Show progress bar. Defaults to True
34
- **kwargs: Keyword arguments. Can be single values or iterables
35
-
36
- Returns:
37
- list[any]: Function output
38
-
39
- Examples:
40
- >>> # Single iterable argument
41
- >>> run_parallel(func, [1,2,3], fixed_arg=42)
42
-
43
- >>> # Multiple iterables in args and kwargs
44
- >>> run_parallel(func, [1,2,3], val=[7,8,9], fixed=42)
45
-
46
- >>> # Only kwargs iterables
47
- >>> run_parallel(func, x=[1,2,3], y=[4,5,6], fixed=42)
48
- """
49
- parallel_kwargs = {"n_jobs": n_jobs, "backend": backend, "verbose": 0}
50
-
51
- iterables = []
52
- fixed_args = []
53
- iterable_kwargs = {}
54
- fixed_kwargs = {}
55
-
56
- first_iterable_len = None
57
-
58
- for arg in args:
59
- if isinstance(arg, (list, tuple)) and not isinstance(arg[0], (list, tuple)):
60
- iterables.append(arg)
61
- if first_iterable_len is None:
62
- first_iterable_len = len(arg)
63
- elif len(arg) != first_iterable_len:
64
- raise ValueError(
65
- f"Iterable length mismatch: argument has length {len(arg)}, expected {first_iterable_len}"
66
- )
67
- else:
68
- fixed_args.append(arg)
69
-
70
- for key, value in kwargs.items():
71
- if isinstance(value, (list, tuple)) and not isinstance(
72
- value[0], (list, tuple)
73
- ):
74
- if first_iterable_len is None:
75
- first_iterable_len = len(value)
76
- elif len(value) != first_iterable_len:
77
- raise ValueError(
78
- f"Iterable length mismatch: {key} has length {len(value)}, expected {first_iterable_len}"
79
- )
80
- iterable_kwargs[key] = value
81
- else:
82
- fixed_kwargs[key] = value
83
-
84
- if first_iterable_len is None:
85
- raise ValueError("At least one iterable argument is required")
86
-
87
- all_iterables = iterables + list(iterable_kwargs.values())
88
- param_combinations = list(zip(*all_iterables))
89
-
90
- if not verbose:
91
- return Parallel(**parallel_kwargs)(
92
- delayed(func)(
93
- *(list(param_tuple[: len(iterables)]) + fixed_args),
94
- **{
95
- k: v
96
- for k, v in zip(
97
- iterable_kwargs.keys(), param_tuple[len(iterables) :]
98
- )
99
- },
100
- **fixed_kwargs,
101
- )
102
- for param_tuple in param_combinations
103
- )
104
- else:
105
- results = [None] * len(param_combinations)
106
- with Progress(
107
- TextColumn("[progress.description]{task.description}"),
108
- BarColumn(),
109
- "[progress.percentage]{task.percentage:>3.0f}%",
110
- TimeElapsedColumn(),
111
- transient=True,
112
- ) as progress:
113
- task = progress.add_task(
114
- "Running in parallel...", total=len(param_combinations)
115
- )
116
-
117
- def wrapper(idx, param_tuple):
118
- res = func(
119
- *(list(param_tuple[: len(iterables)]) + fixed_args),
120
- **{
121
- k: v
122
- for k, v in zip(
123
- iterable_kwargs.keys(), param_tuple[len(iterables) :]
124
- )
125
- },
126
- **fixed_kwargs,
127
- )
128
- progress.update(task, advance=1)
129
- return idx, res
130
-
131
- for idx, result in Parallel(**parallel_kwargs)(
132
- delayed(wrapper)(i, param_tuple)
133
- for i, param_tuple in enumerate(param_combinations)
134
- ):
135
- results[idx] = result
136
- return results
137
-
138
- else:
139
-
140
- def run_parallel(*args, **kwargs):
141
- raise ImportError("joblib not installed")
11
+ from .security import validate_file_path
12
+ from fsspec_utils.utils import run_parallel
13
+
14
+ # if importlib.util.find_spec("joblib"):
15
+ # from joblib import Parallel, delayed
16
+ # from rich.progress import (BarColumn, Progress, TextColumn,
17
+ # TimeElapsedColumn)
18
+
19
+ # def _prepare_parallel_args(
20
+ # args: tuple, kwargs: dict
21
+ # ) -> tuple[list, list, dict, dict, int]:
22
+ # """Prepare and validate arguments for parallel execution.
23
+
24
+ # Args:
25
+ # args: Positional arguments
26
+ # kwargs: Keyword arguments
27
+
28
+ # Returns:
29
+ # tuple: (iterables, fixed_args, iterable_kwargs, fixed_kwargs, first_iterable_len)
30
+
31
+ # Raises:
32
+ # ValueError: If no iterable arguments or length mismatch
33
+ # """
34
+ # iterables = []
35
+ # fixed_args = []
36
+ # iterable_kwargs = {}
37
+ # fixed_kwargs = {}
38
+ # first_iterable_len = None
39
+
40
+ # # Process positional arguments
41
+ # for arg in args:
42
+ # if isinstance(arg, (list, tuple)) and not isinstance(arg[0], (list, tuple)):
43
+ # iterables.append(arg)
44
+ # if first_iterable_len is None:
45
+ # first_iterable_len = len(arg)
46
+ # elif len(arg) != first_iterable_len:
47
+ # raise ValueError(
48
+ # f"Iterable length mismatch: argument has length {len(arg)}, expected {first_iterable_len}"
49
+ # )
50
+ # else:
51
+ # fixed_args.append(arg)
52
+
53
+ # # Process keyword arguments
54
+ # for key, value in kwargs.items():
55
+ # if isinstance(value, (list, tuple)) and not isinstance(
56
+ # value[0], (list, tuple)
57
+ # ):
58
+ # if first_iterable_len is None:
59
+ # first_iterable_len = len(value)
60
+ # elif len(value) != first_iterable_len:
61
+ # raise ValueError(
62
+ # f"Iterable length mismatch: {key} has length {len(value)}, expected {first_iterable_len}"
63
+ # )
64
+ # iterable_kwargs[key] = value
65
+ # else:
66
+ # fixed_kwargs[key] = value
67
+
68
+ # if first_iterable_len is None:
69
+ # raise ValueError("At least one iterable argument is required")
70
+
71
+ # return iterables, fixed_args, iterable_kwargs, fixed_kwargs, first_iterable_len
72
+
73
+ # def _execute_parallel_with_progress(
74
+ # func: callable,
75
+ # iterables: list,
76
+ # fixed_args: list,
77
+ # iterable_kwargs: dict,
78
+ # fixed_kwargs: dict,
79
+ # param_combinations: list,
80
+ # parallel_kwargs: dict,
81
+ # ) -> list:
82
+ # """Execute parallel tasks with progress tracking.
83
+
84
+ # Args:
85
+ # func: Function to execute
86
+ # iterables: List of iterable arguments
87
+ # fixed_args: List of fixed arguments
88
+ # iterable_kwargs: Dictionary of iterable keyword arguments
89
+ # fixed_kwargs: Dictionary of fixed keyword arguments
90
+ # param_combinations: List of parameter combinations
91
+ # parallel_kwargs: Parallel execution configuration
92
+
93
+ # Returns:
94
+ # list: Results from parallel execution
95
+ # """
96
+ # results = [None] * len(param_combinations)
97
+ # with Progress(
98
+ # TextColumn("[progress.description]{task.description}"),
99
+ # BarColumn(),
100
+ # "[progress.percentage]{task.percentage:>3.0f}%",
101
+ # TimeElapsedColumn(),
102
+ # transient=True,
103
+ # ) as progress:
104
+ # task = progress.add_task(
105
+ # "Running in parallel...", total=len(param_combinations)
106
+ # )
107
+
108
+ # def wrapper(idx, param_tuple):
109
+ # res = func(
110
+ # *(list(param_tuple[: len(iterables)]) + fixed_args),
111
+ # **{
112
+ # k: v
113
+ # for k, v in zip(
114
+ # iterable_kwargs.keys(), param_tuple[len(iterables) :]
115
+ # )
116
+ # },
117
+ # **fixed_kwargs,
118
+ # )
119
+ # progress.update(task, advance=1)
120
+ # return idx, res
121
+ #
122
+ # for idx, result in Parallel(**parallel_kwargs)(
123
+ # delayed(wrapper)(i, param_tuple)
124
+ # for i, param_tuple in enumerate(param_combinations)
125
+ # ):
126
+ # results[idx] = result
127
+ # return results
128
+
129
+ # def _execute_parallel_without_progress(
130
+ # func: callable,
131
+ # iterables: list,
132
+ # fixed_args: list,
133
+ # iterable_kwargs: dict,
134
+ # fixed_kwargs: dict,
135
+ # param_combinations: list,
136
+ # parallel_kwargs: dict,
137
+ # ) -> list:
138
+ # """Execute parallel tasks without progress tracking.
139
+
140
+ # Args:
141
+ # func: Function to execute
142
+ # iterables: List of iterable arguments
143
+ # fixed_args: List of fixed arguments
144
+ # iterable_kwargs: Dictionary of iterable keyword arguments
145
+ # fixed_kwargs: Dictionary of fixed keyword arguments
146
+ # param_combinations: List of parameter combinations
147
+ # parallel_kwargs: Parallel execution configuration
148
+
149
+ # Returns:
150
+ # list: Results from parallel execution
151
+ # """
152
+ # return Parallel(**parallel_kwargs)(
153
+ # delayed(func)(
154
+ # *(list(param_tuple[: len(iterables)]) + fixed_args),
155
+ # **{
156
+ # k: v
157
+ # for k, v in zip(
158
+ # iterable_kwargs.keys(), param_tuple[len(iterables) :]
159
+ # )
160
+ # },
161
+ # **fixed_kwargs,
162
+ # )
163
+ # for param_tuple in param_combinations
164
+ # )
165
+
166
+ # def run_parallel(
167
+ # func: callable,
168
+ # *args,
169
+ # n_jobs: int = -1,
170
+ # backend: str = "threading",
171
+ # verbose: bool = True,
172
+ # **kwargs,
173
+ # ) -> list[any]:
174
+ # """Runs a function for a list of parameters in parallel.
175
+
176
+ # Args:
177
+ # func (Callable): function to run in parallel
178
+ # *args: Positional arguments. Can be single values or iterables
179
+ # n_jobs (int, optional): Number of joblib workers. Defaults to -1
180
+ # backend (str, optional): joblib backend. Valid options are
181
+ # `loky`,`threading`, `mutliprocessing` or `sequential`. Defaults to "threading"
182
+ # verbose (bool, optional): Show progress bar. Defaults to True
183
+ # **kwargs: Keyword arguments. Can be single values or iterables
184
+
185
+ # Returns:
186
+ # list[any]: Function output
187
+
188
+ # Examples:
189
+ # >>> # Single iterable argument
190
+ # >>> run_parallel(func, [1,2,3], fixed_arg=42)
191
+
192
+ # >>> # Multiple iterables in args and kwargs
193
+ # >>> run_parallel(func, [1,2,3], val=[7,8,9], fixed=42)
194
+
195
+ # >>> # Only kwargs iterables
196
+ # >>> run_parallel(func, x=[1,2,3], y=[4,5,6], fixed=42)
197
+ # """
198
+ # parallel_kwargs = {"n_jobs": n_jobs, "backend": backend, "verbose": 0}
199
+
200
+ # # Prepare and validate arguments
201
+ # iterables, fixed_args, iterable_kwargs, fixed_kwargs, first_iterable_len = _prepare_parallel_args(
202
+ # args, kwargs
203
+ # )
204
+
205
+ # # Create parameter combinations
206
+ # all_iterables = iterables + list(iterable_kwargs.values())
207
+ # param_combinations = list(zip(*all_iterables))
208
+
209
+ # # Execute with or without progress tracking
210
+ # if not verbose:
211
+ # return _execute_parallel_without_progress(
212
+ # func, iterables, fixed_args, iterable_kwargs, fixed_kwargs,
213
+ # param_combinations, parallel_kwargs
214
+ # )
215
+ # else:
216
+ # return _execute_parallel_with_progress(
217
+ # func, iterables, fixed_args, iterable_kwargs, fixed_kwargs,
218
+ # param_combinations, parallel_kwargs
219
+ # )
220
+
221
+ # else:
222
+
223
+ # def run_parallel(*args, **kwargs):
224
+ # raise ImportError("joblib not installed")
142
225
 
143
226
 
144
227
  def get_partitions_from_path(
@@ -170,19 +253,110 @@ def get_partitions_from_path(
170
253
  return list(zip(partitioning, parts[-len(partitioning) :]))
171
254
 
172
255
 
173
- def view_img(data: str | bytes, format: str = "svg"):
174
- # Create a temporary file with .svg extension
256
+ def _validate_image_format(format: str) -> str:
257
+ """Validate image format to prevent injection attacks.
258
+
259
+ Args:
260
+ format: Image format to validate
261
+
262
+ Returns:
263
+ str: Validated format
264
+
265
+ Raises:
266
+ ValueError: If format is not supported
267
+ """
268
+ allowed_formats = {"svg", "png", "jpg", "jpeg", "gif", "pdf", "html"}
269
+ if format not in allowed_formats:
270
+ raise ValueError(f"Unsupported format: {format}. Allowed: {allowed_formats}")
271
+ return format
272
+
273
+ def _create_temp_image_file(data: str | bytes, format: str) -> str:
274
+ """Create a temporary file with image data.
275
+
276
+ Args:
277
+ data: Image data as string or bytes
278
+ format: Validated image format
279
+
280
+ Returns:
281
+ str: Path to temporary file
282
+
283
+ Raises:
284
+ OSError: If file creation fails
285
+ """
175
286
  with tempfile.NamedTemporaryFile(suffix=f".{format}", delete=False) as tmp:
176
- tmp.write(data)
287
+ if isinstance(data, str):
288
+ tmp.write(data.encode('utf-8'))
289
+ else:
290
+ tmp.write(data)
177
291
  tmp_path = tmp.name
292
+
293
+ # Validate the temporary file path for security
294
+ validate_file_path(tmp_path, allow_relative=False)
295
+ return tmp_path
178
296
 
179
- # Open with default application on macOS
180
- subprocess.run(["open", tmp_path])
297
+ def _open_image_viewer(tmp_path: str) -> None:
298
+ """Open image viewer with the given file path.
299
+
300
+ Args:
301
+ tmp_path: Path to temporary image file
302
+
303
+ Raises:
304
+ OSError: If platform is not supported
305
+ subprocess.CalledProcessError: If subprocess fails
306
+ subprocess.TimeoutExpired: If subprocess times out
307
+ """
308
+ import platform
309
+ platform_system = platform.system()
310
+
311
+ if platform_system == "Darwin": # macOS
312
+ subprocess.run(["open", tmp_path], check=True, timeout=10)
313
+ elif platform_system == "Linux":
314
+ subprocess.run(["xdg-open", tmp_path], check=True, timeout=10)
315
+ elif platform_system == "Windows":
316
+ subprocess.run(["start", "", tmp_path], shell=True, check=True, timeout=10)
317
+ else:
318
+ raise OSError(f"Unsupported platform: {platform_system}")
181
319
 
182
- # Optional: Remove the temp file after a delay
320
+ def _cleanup_temp_file(tmp_path: str) -> None:
321
+ """Clean up temporary file.
322
+
323
+ Args:
324
+ tmp_path: Path to temporary file to remove
325
+ """
326
+ try:
327
+ os.unlink(tmp_path)
328
+ except OSError:
329
+ pass # File might already be deleted or in use
330
+
331
+ def view_img(data: str | bytes, format: str = "svg"):
332
+ """View image data using the system's default image viewer.
333
+
334
+ Args:
335
+ data: Image data as string or bytes
336
+ format: Image format (svg, png, jpg, jpeg, gif, pdf, html)
337
+
338
+ Raises:
339
+ ValueError: If format is not supported
340
+ RuntimeError: If file opening fails
341
+ OSError: If platform is not supported
342
+ """
343
+ # Validate format to prevent injection attacks
344
+ validated_format = _validate_image_format(format)
345
+
346
+ # Create a temporary file with validated extension
347
+ tmp_path = _create_temp_image_file(data, validated_format)
183
348
 
349
+ try:
350
+ # Open image viewer with secure subprocess call
351
+ _open_image_viewer(tmp_path)
352
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError) as e:
353
+ # Clean up temp file on error
354
+ _cleanup_temp_file(tmp_path)
355
+ raise RuntimeError(f"Failed to open file: {e}")
356
+
357
+ # Optional: Remove the temp file after a delay
184
358
  time.sleep(2) # Wait for viewer to open
185
- os.unlink(tmp_path)
359
+ _cleanup_temp_file(tmp_path)
186
360
 
187
361
 
188
362
  def update_config_from_dict(