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
@@ -0,0 +1,286 @@
1
+ """
2
+ Adapter utilities for FlowerPower pipeline management.
3
+
4
+ This module provides helper classes for managing adapter configurations
5
+ and creating adapter instances with proper error handling and validation.
6
+ """
7
+
8
+ import sys
9
+ from typing import Any, Dict, Optional
10
+
11
+ import msgspec
12
+ from loguru import logger
13
+
14
+
15
+ class AdapterManager:
16
+ """
17
+ Helper class for adapter configuration and instance creation.
18
+
19
+ This class centralizes adapter configuration merging, validation,
20
+ and instance creation to reduce complexity in the Pipeline class.
21
+ """
22
+
23
+ def __init__(self):
24
+ """Initialize the adapter manager."""
25
+ self._adapter_cache: Dict[str, Any] = {}
26
+
27
+ def _merge_configs(self, base_config: Any, override_config: Any) -> Any:
28
+ """Merge override config into base config."""
29
+ if not override_config:
30
+ return base_config
31
+ return base_config.merge(override_config) if base_config else override_config
32
+
33
+ def resolve_with_adapter_config(
34
+ self,
35
+ with_adapter_cfg: dict | Any | None,
36
+ base_config: Any
37
+ ) -> Any:
38
+ """
39
+ Resolve and merge WithAdapterConfig.
40
+
41
+ Args:
42
+ with_adapter_cfg: Input configuration (dict or instance)
43
+ base_config: Base configuration to merge with
44
+
45
+ Returns:
46
+ WithAdapterConfig: Merged configuration
47
+ """
48
+ from ..cfg.pipeline.run import WithAdapterConfig
49
+
50
+ if with_adapter_cfg:
51
+ if isinstance(with_adapter_cfg, dict):
52
+ with_adapter_cfg = WithAdapterConfig.from_dict(with_adapter_cfg)
53
+ elif not isinstance(with_adapter_cfg, WithAdapterConfig):
54
+ raise TypeError(
55
+ "with_adapter must be a dictionary or WithAdapterConfig instance."
56
+ )
57
+
58
+ return self._merge_configs(base_config, with_adapter_cfg)
59
+
60
+ return base_config
61
+
62
+ def resolve_pipeline_adapter_config(
63
+ self,
64
+ pipeline_adapter_cfg: dict | Any | None,
65
+ base_config: Any
66
+ ) -> Any:
67
+ """
68
+ Resolve and merge PipelineAdapterConfig.
69
+
70
+ Args:
71
+ pipeline_adapter_cfg: Input configuration (dict or instance)
72
+ base_config: Base configuration to merge with
73
+
74
+ Returns:
75
+ PipelineAdapterConfig: Merged configuration
76
+ """
77
+ from ..cfg.pipeline.adapter import AdapterConfig as PipelineAdapterConfig
78
+
79
+ if pipeline_adapter_cfg:
80
+ if isinstance(pipeline_adapter_cfg, dict):
81
+ pipeline_adapter_cfg = PipelineAdapterConfig.from_dict(
82
+ pipeline_adapter_cfg
83
+ )
84
+ elif not isinstance(pipeline_adapter_cfg, PipelineAdapterConfig):
85
+ raise TypeError(
86
+ "pipeline_adapter_cfg must be a dictionary or PipelineAdapterConfig instance."
87
+ )
88
+
89
+ return self._merge_configs(base_config, pipeline_adapter_cfg)
90
+
91
+ return base_config
92
+
93
+ def resolve_project_adapter_config(
94
+ self,
95
+ project_adapter_cfg: dict | Any | None,
96
+ project_context: Any
97
+ ) -> Any:
98
+ """
99
+ Resolve and merge ProjectAdapterConfig from project context.
100
+
101
+ Args:
102
+ project_adapter_cfg: Input configuration (dict or instance)
103
+ project_context: Project context to extract base config from
104
+
105
+ Returns:
106
+ ProjectAdapterConfig: Merged configuration
107
+ """
108
+ from ..cfg.project.adapter import AdapterConfig as ProjectAdapterConfig
109
+
110
+ # Get base configuration from project context
111
+ base_cfg = self._extract_project_adapter_config(project_context)
112
+
113
+ if project_adapter_cfg:
114
+ if isinstance(project_adapter_cfg, dict):
115
+ project_adapter_cfg = ProjectAdapterConfig.from_dict(
116
+ project_adapter_cfg
117
+ )
118
+ elif not isinstance(project_adapter_cfg, ProjectAdapterConfig):
119
+ raise TypeError(
120
+ "project_adapter_cfg must be a dictionary or ProjectAdapterConfig instance."
121
+ )
122
+
123
+ return self._merge_configs(base_cfg, project_adapter_cfg)
124
+ else:
125
+ # Use base configuration or create default
126
+ return base_cfg or ProjectAdapterConfig()
127
+
128
+ def _extract_project_adapter_config(
129
+ self,
130
+ project_context: Any
131
+ ) -> Optional[Any]:
132
+ """
133
+ Extract adapter configuration from project context.
134
+
135
+ Args:
136
+ project_context: Project context (PipelineManager or FlowerPowerProject)
137
+
138
+ Returns:
139
+ ProjectAdapterConfig or None: Extracted configuration
140
+ """
141
+ # Try direct access to project config
142
+ project_cfg = getattr(project_context, "project_cfg", None) or getattr(project_context, "_project_cfg", None)
143
+ if project_cfg and hasattr(project_cfg, "adapter"):
144
+ return project_cfg.adapter
145
+
146
+ # Try via pipeline_manager if available
147
+ if hasattr(project_context, "pipeline_manager"):
148
+ pm = project_context.pipeline_manager
149
+ pm_cfg = getattr(pm, "project_cfg", None) or getattr(pm, "_project_cfg", None)
150
+ if pm_cfg and hasattr(pm_cfg, "adapter"):
151
+ return pm_cfg.adapter
152
+
153
+ return None
154
+
155
+ def create_adapters(
156
+ self,
157
+ with_adapter_cfg: Any,
158
+ pipeline_adapter_cfg: Any,
159
+ project_adapter_cfg: Any
160
+ ) -> list:
161
+ """
162
+ Create adapter instances based on configurations.
163
+
164
+ Args:
165
+ with_adapter_cfg: WithAdapter configuration
166
+ pipeline_adapter_cfg: Pipeline adapter configuration
167
+ project_adapter_cfg: Project adapter configuration
168
+
169
+ Returns:
170
+ list: List of adapter instances
171
+ """
172
+ adapters = []
173
+
174
+ # Hamilton Tracker adapter
175
+ if with_adapter_cfg.hamilton_tracker:
176
+ adapter = self._create_hamilton_tracker(
177
+ pipeline_adapter_cfg.hamilton_tracker,
178
+ project_adapter_cfg.hamilton_tracker
179
+ )
180
+ if adapter:
181
+ adapters.append(adapter)
182
+
183
+ # MLFlow adapter
184
+ if with_adapter_cfg.mlflow:
185
+ adapter = self._create_mlflow_adapter(
186
+ pipeline_adapter_cfg.mlflow,
187
+ project_adapter_cfg.mlflow
188
+ )
189
+ if adapter:
190
+ adapters.append(adapter)
191
+
192
+ # OpenTelemetry adapter
193
+ if with_adapter_cfg.opentelemetry:
194
+ adapter = self._create_opentelemetry_adapter(
195
+ pipeline_adapter_cfg.opentelemetry,
196
+ project_adapter_cfg.opentelemetry
197
+ )
198
+ if adapter:
199
+ adapters.append(adapter)
200
+
201
+ return adapters
202
+
203
+ def _create_hamilton_tracker(
204
+ self,
205
+ pipeline_config: Any,
206
+ project_config: Any
207
+ ) -> Optional[Any]:
208
+ """Create HamiltonTracker adapter instance."""
209
+ try:
210
+ from hamilton.adapters import HamiltonTracker
211
+ from hamilton import constants
212
+ from ..settings import settings
213
+ except ImportError:
214
+ logger.warning("Hamilton tracker dependencies not installed")
215
+ return None
216
+
217
+ tracker_kwargs = project_config.to_dict()
218
+ tracker_kwargs.update(pipeline_config.to_dict())
219
+ tracker_kwargs["hamilton_api_url"] = tracker_kwargs.pop("api_url", None)
220
+ tracker_kwargs["hamilton_ui_url"] = tracker_kwargs.pop("ui_url", None)
221
+
222
+ # Set capture constants
223
+ constants.MAX_DICT_LENGTH_CAPTURE = (
224
+ tracker_kwargs.pop("max_dict_length_capture", None)
225
+ or settings.HAMILTON_MAX_DICT_LENGTH_CAPTURE
226
+ )
227
+ constants.MAX_LIST_LENGTH_CAPTURE = (
228
+ tracker_kwargs.pop("max_list_length_capture", None)
229
+ or settings.HAMILTON_MAX_LIST_LENGTH_CAPTURE
230
+ )
231
+ constants.CAPTURE_DATA_STATISTICS = (
232
+ tracker_kwargs.pop("capture_data_statistics", None)
233
+ or settings.HAMILTON_CAPTURE_DATA_STATISTICS
234
+ )
235
+
236
+ return HamiltonTracker(**tracker_kwargs)
237
+
238
+ def _create_mlflow_adapter(
239
+ self,
240
+ pipeline_config: Any,
241
+ project_config: Any
242
+ ) -> Optional[Any]:
243
+ """Create MLFlow adapter instance."""
244
+ try:
245
+ from hamilton.experimental import h_mlflow
246
+ except ImportError:
247
+ logger.warning("MLFlow is not installed. Skipping MLFlow adapter.")
248
+ return None
249
+
250
+ mlflow_kwargs = project_config.to_dict()
251
+ mlflow_kwargs.update(pipeline_config.to_dict())
252
+ return h_mlflow.MLFlowTracker(**mlflow_kwargs)
253
+
254
+ def _create_opentelemetry_adapter(
255
+ self,
256
+ pipeline_config: Any,
257
+ project_config: Any
258
+ ) -> Optional[Any]:
259
+ """Create OpenTelemetry adapter instance."""
260
+ try:
261
+ from hamilton.experimental import h_opentelemetry
262
+ from ..utils.open_telemetry import init_tracer
263
+ except ImportError:
264
+ logger.warning(
265
+ "OpenTelemetry is not installed. Skipping OpenTelemetry adapter."
266
+ )
267
+ return None
268
+
269
+ otel_kwargs = project_config.to_dict()
270
+ otel_kwargs.update(pipeline_config.to_dict())
271
+ init_tracer()
272
+ return h_opentelemetry.OpenTelemetryTracker(**otel_kwargs)
273
+
274
+ def clear_cache(self) -> None:
275
+ """Clear the adapter cache."""
276
+ self._adapter_cache.clear()
277
+
278
+
279
+ def create_adapter_manager() -> AdapterManager:
280
+ """
281
+ Factory function to create an AdapterManager instance.
282
+
283
+ Returns:
284
+ AdapterManager: Configured manager instance
285
+ """
286
+ return AdapterManager()
@@ -14,80 +14,86 @@ from .logging import setup_logging
14
14
  setup_logging(level=LOG_LEVEL)
15
15
 
16
16
 
17
- def _execute_callback(callback_info: Any, context_exception: Exception = None):
18
- """
19
- Helper to execute a callback.
20
- The callback_info can be a callable, or a tuple (callable, args_tuple, kwargs_dict).
21
- If context_exception is provided (for on_failure), it can be passed to the callback.
22
- """
23
- if not callback_info:
24
- return
17
+ def _add_exception_to_simple_callback(callback_fn: Callable, context_exception: Exception, cb_args: list, cb_kwargs: Dict[str, Any]):
18
+ """Add exception to simple callback arguments."""
19
+ try:
20
+ sig = inspect.signature(callback_fn)
21
+ if len(sig.parameters) == 1:
22
+ first_param = next(iter(sig.parameters.values()))
23
+ if first_param.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.POSITIONAL_ONLY):
24
+ cb_args.append(context_exception)
25
+ elif "exception" in sig.parameters:
26
+ cb_kwargs["exception"] = context_exception
27
+ except (ValueError, TypeError):
28
+ logger.debug(
29
+ f"Could not inspect signature for simple callback {getattr(callback_fn, '__name__', str(callback_fn))}. Exception not passed automatically."
30
+ )
31
+
32
+
33
+ def _parse_tuple_callback_args(callback_info: tuple, cb_args: list, cb_kwargs: Dict[str, Any]):
34
+ """Parse args and kwargs from tuple callback info."""
35
+ callback_fn = callback_info[0]
36
+
37
+ # Args: callback_info[1]
38
+ if len(callback_info) > 1 and callback_info[1] is not None:
39
+ if isinstance(callback_info[1], tuple):
40
+ cb_args.extend(callback_info[1])
41
+ else:
42
+ logger.warning(
43
+ f"Callback args for {getattr(callback_fn, '__name__', str(callback_fn))} "
44
+ f"expected tuple, got {type(callback_info[1])}. Ignoring args."
45
+ )
46
+
47
+ # Kwargs: callback_info[2]
48
+ if len(callback_info) > 2 and callback_info[2] is not None:
49
+ if isinstance(callback_info[2], dict):
50
+ cb_kwargs.update(callback_info[2])
51
+ else:
52
+ logger.warning(
53
+ f"Callback kwargs for {getattr(callback_fn, '__name__', str(callback_fn))} "
54
+ f"expected dict, got {type(callback_info[2])}. Ignoring kwargs."
55
+ )
56
+
25
57
 
26
- callback_fn: Callable = None
27
- cb_args: Tuple = ()
28
- cb_kwargs: Dict[str, Any] = {}
58
+ def _add_exception_to_tuple_callback(callback_fn: Callable, context_exception: Exception, cb_kwargs: Dict[str, Any]):
59
+ """Add exception to tuple callback kwargs if accepted."""
60
+ try:
61
+ sig = inspect.signature(callback_fn)
62
+ if "exception" in sig.parameters:
63
+ cb_kwargs["exception"] = context_exception
64
+ except (ValueError, TypeError):
65
+ pass
29
66
 
30
- is_simple_callable = isinstance(callback_info, Callable)
31
67
 
32
- if is_simple_callable:
68
+ def _prepare_callback_details(callback_info: Any, context_exception: Exception = None) -> tuple[Callable | None, tuple, Dict[str, Any]]:
69
+ """Prepare callback function and arguments for execution."""
70
+ if not callback_info:
71
+ return None, (), {}
72
+
73
+ callback_fn = None
74
+ cb_args = []
75
+ cb_kwargs = {}
76
+
77
+ if isinstance(callback_info, Callable):
33
78
  callback_fn = callback_info
34
- # For a simple callable in an on_failure context, try to pass the exception.
35
79
  if context_exception:
36
- try:
37
- sig = inspect.signature(callback_fn)
38
- if len(sig.parameters) == 1: # Assumes it takes one positional argument
39
- first_param_name = list(sig.parameters.keys())[0]
40
- # Avoid passing if it's a **kwargs style param and we have no other indication
41
- if sig.parameters[first_param_name].kind in [
42
- inspect.Parameter.POSITIONAL_OR_KEYWORD,
43
- inspect.Parameter.POSITIONAL_ONLY,
44
- ]:
45
- cb_args = (context_exception,)
46
- elif (
47
- "exception" in sig.parameters
48
- ): # Or if it explicitly takes 'exception' kwarg
49
- cb_kwargs["exception"] = context_exception
50
- except (ValueError, TypeError): # Some callables might not be inspectable
51
- logger.debug(
52
- f"Could not inspect signature for simple callback {getattr(callback_fn, '__name__', str(callback_fn))}. Exception not passed automatically."
53
- )
54
-
55
- elif (
56
- isinstance(callback_info, tuple)
57
- and len(callback_info) > 0
58
- and isinstance(callback_info[0], Callable)
59
- ):
80
+ _add_exception_to_simple_callback(callback_fn, context_exception, cb_args, cb_kwargs)
81
+ elif isinstance(callback_info, tuple) and len(callback_info) > 0 and isinstance(callback_info[0], Callable):
60
82
  callback_fn = callback_info[0]
61
-
62
- # Args: callback_info[1]
63
- if len(callback_info) > 1 and callback_info[1] is not None:
64
- if isinstance(callback_info[1], tuple):
65
- cb_args = callback_info[1]
66
- else:
67
- logger.warning(
68
- f"Callback args for {getattr(callback_fn, '__name__', str(callback_fn))} "
69
- f"expected tuple, got {type(callback_info[1])}. Ignoring args."
70
- )
71
-
72
- # Kwargs: callback_info[2]
73
- if len(callback_info) > 2 and callback_info[2] is not None:
74
- if isinstance(callback_info[2], dict):
75
- cb_kwargs = callback_info[2].copy() # Use a copy
76
- else:
77
- logger.warning(
78
- f"Callback kwargs for {getattr(callback_fn, '__name__', str(callback_fn))} "
79
- f"expected dict, got {type(callback_info[2])}. Ignoring kwargs."
80
- )
81
-
82
- # If this is an on_failure call and an exception occurred,
83
- # pass it if 'exception' kwarg is not set and the callback accepts it.
83
+ _parse_tuple_callback_args(callback_info, cb_args, cb_kwargs)
84
84
  if context_exception and "exception" not in cb_kwargs:
85
- try:
86
- sig = inspect.signature(callback_fn)
87
- if "exception" in sig.parameters:
88
- cb_kwargs["exception"] = context_exception
89
- except (ValueError, TypeError): # Some callables might not be inspectable
90
- pass # Cannot inspect, so don't add
85
+ _add_exception_to_tuple_callback(callback_fn, context_exception, cb_kwargs)
86
+
87
+ return callback_fn, tuple(cb_args), cb_kwargs
88
+
89
+
90
+ def _execute_callback(callback_info: Any, context_exception: Exception = None):
91
+ """
92
+ Helper to execute a callback.
93
+ The callback_info can be a callable, or a tuple (callable, args_tuple, kwargs_dict).
94
+ If context_exception is provided (for on_failure), it can be passed to the callback.
95
+ """
96
+ callback_fn, cb_args, cb_kwargs = _prepare_callback_details(callback_info, context_exception)
91
97
 
92
98
  if callback_fn:
93
99
  try: