FlowerPower 0.30.0__py3-none-any.whl → 0.31.0__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.
- flowerpower/cfg/__init__.py +143 -25
- flowerpower/cfg/base.py +132 -11
- flowerpower/cfg/exceptions.py +53 -0
- flowerpower/cfg/pipeline/__init__.py +151 -35
- flowerpower/cfg/pipeline/adapter.py +1 -0
- flowerpower/cfg/pipeline/builder.py +24 -25
- flowerpower/cfg/pipeline/builder_adapter.py +142 -0
- flowerpower/cfg/pipeline/builder_executor.py +101 -0
- flowerpower/cfg/pipeline/run.py +99 -40
- flowerpower/cfg/project/__init__.py +59 -14
- flowerpower/cfg/project/adapter.py +6 -0
- flowerpower/cli/__init__.py +8 -2
- flowerpower/cli/cfg.py +0 -38
- flowerpower/cli/pipeline.py +121 -83
- flowerpower/cli/utils.py +120 -71
- flowerpower/flowerpower.py +94 -120
- flowerpower/pipeline/config_manager.py +180 -0
- flowerpower/pipeline/executor.py +126 -0
- flowerpower/pipeline/lifecycle_manager.py +231 -0
- flowerpower/pipeline/manager.py +121 -274
- flowerpower/pipeline/pipeline.py +66 -278
- flowerpower/pipeline/registry.py +45 -4
- flowerpower/utils/__init__.py +19 -0
- flowerpower/utils/adapter.py +286 -0
- flowerpower/utils/callback.py +73 -67
- flowerpower/utils/config.py +306 -0
- flowerpower/utils/executor.py +178 -0
- flowerpower/utils/filesystem.py +194 -0
- flowerpower/utils/misc.py +249 -76
- flowerpower/utils/security.py +221 -0
- {flowerpower-0.30.0.dist-info → flowerpower-0.31.0.dist-info}/METADATA +2 -2
- flowerpower-0.31.0.dist-info/RECORD +53 -0
- flowerpower/cfg/pipeline/_schedule.py +0 -32
- flowerpower-0.30.0.dist-info/RECORD +0 -42
- {flowerpower-0.30.0.dist-info → flowerpower-0.31.0.dist-info}/WHEEL +0 -0
- {flowerpower-0.30.0.dist-info → flowerpower-0.31.0.dist-info}/entry_points.txt +0 -0
- {flowerpower-0.30.0.dist-info → flowerpower-0.31.0.dist-info}/licenses/LICENSE +0 -0
- {flowerpower-0.30.0.dist-info → flowerpower-0.31.0.dist-info}/top_level.txt +0 -0
flowerpower/cfg/pipeline/run.py
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
+
import warnings
|
1
2
|
import msgspec
|
3
|
+
import importlib
|
2
4
|
from munch import munchify
|
3
5
|
from typing import Any, Callable
|
4
|
-
|
6
|
+
from requests.exceptions import HTTPError, ConnectionError, Timeout # Example exception
|
5
7
|
from ... import settings
|
6
8
|
from ..base import BaseConfig
|
7
9
|
|
@@ -22,25 +24,6 @@ class ExecutorConfig(BaseConfig):
|
|
22
24
|
num_cpus: int | None = msgspec.field(default=settings.EXECUTOR_NUM_CPUS)
|
23
25
|
|
24
26
|
|
25
|
-
class RunConfig(BaseConfig):
|
26
|
-
inputs: dict | None = msgspec.field(default_factory=dict)
|
27
|
-
final_vars: list[str] | None = msgspec.field(default_factory=list)
|
28
|
-
config: dict | None = msgspec.field(default_factory=dict)
|
29
|
-
cache: dict | bool | None = msgspec.field(default=False)
|
30
|
-
with_adapter: WithAdapterConfig = msgspec.field(default_factory=WithAdapterConfig)
|
31
|
-
executor: ExecutorConfig = msgspec.field(default_factory=ExecutorConfig)
|
32
|
-
log_level: str | None = msgspec.field(default="INFO")
|
33
|
-
max_retries: int = msgspec.field(default=3)
|
34
|
-
retry_delay: int | float = msgspec.field(default=1)
|
35
|
-
jitter_factor: float | None = msgspec.field(default=0.1)
|
36
|
-
retry_exceptions: list[str] = msgspec.field(default_factory=lambda: ["Exception"])
|
37
|
-
# New fields for comprehensive configuration
|
38
|
-
pipeline_adapter_cfg: dict | None = msgspec.field(default=None)
|
39
|
-
project_adapter_cfg: dict | None = msgspec.field(default=None)
|
40
|
-
adapter: dict[str, Any] | None = msgspec.field(default=None)
|
41
|
-
reload: bool = msgspec.field(default=False)
|
42
|
-
|
43
|
-
|
44
27
|
class CallbackSpec(msgspec.Struct):
|
45
28
|
"""Specification for a callback function with optional arguments."""
|
46
29
|
func: Callable
|
@@ -69,8 +52,8 @@ class RunConfig(BaseConfig):
|
|
69
52
|
on_failure: CallbackSpec | None = msgspec.field(default=None)
|
70
53
|
|
71
54
|
def __post_init__(self):
|
72
|
-
if isinstance(self.inputs, dict):
|
73
|
-
|
55
|
+
# if isinstance(self.inputs, dict):
|
56
|
+
# self.inputs = munchify(self.inputs)
|
74
57
|
if isinstance(self.config, dict):
|
75
58
|
self.config = munchify(self.config)
|
76
59
|
if isinstance(self.cache, (dict)):
|
@@ -89,24 +72,8 @@ class RunConfig(BaseConfig):
|
|
89
72
|
# Convert adapter instances if needed
|
90
73
|
pass
|
91
74
|
if isinstance(self.retry_exceptions, list):
|
92
|
-
# Convert string exceptions to actual exception classes
|
93
|
-
|
94
|
-
for exc in self.retry_exceptions:
|
95
|
-
if isinstance(exc, str):
|
96
|
-
try:
|
97
|
-
exc_class = eval(exc)
|
98
|
-
# Ensure it's actually an exception class
|
99
|
-
if isinstance(exc_class, type) and issubclass(exc_class, BaseException):
|
100
|
-
converted_exceptions.append(exc_class)
|
101
|
-
else:
|
102
|
-
converted_exceptions.append(Exception)
|
103
|
-
except (NameError, AttributeError):
|
104
|
-
converted_exceptions.append(Exception)
|
105
|
-
elif isinstance(exc, type) and issubclass(exc, BaseException):
|
106
|
-
converted_exceptions.append(exc)
|
107
|
-
else:
|
108
|
-
converted_exceptions.append(Exception)
|
109
|
-
self.retry_exceptions = converted_exceptions
|
75
|
+
# Convert string exceptions to actual exception classes using dynamic import
|
76
|
+
self.retry_exceptions = self._convert_exception_strings(self.retry_exceptions)
|
110
77
|
|
111
78
|
# Handle callback conversions
|
112
79
|
if self.on_success is not None and not isinstance(self.on_success, CallbackSpec):
|
@@ -121,6 +88,98 @@ class RunConfig(BaseConfig):
|
|
121
88
|
"Invalid on_success format, must be Callable or (Callable, args, kwargs)",
|
122
89
|
RuntimeWarning
|
123
90
|
)
|
91
|
+
|
92
|
+
def _convert_exception_strings(self, exception_list: list) -> list:
|
93
|
+
"""Convert exception strings to actual exception classes using dynamic import.
|
94
|
+
|
95
|
+
Args:
|
96
|
+
exception_list: List of exception names or classes.
|
97
|
+
|
98
|
+
Returns:
|
99
|
+
List of exception classes.
|
100
|
+
"""
|
101
|
+
converted_exceptions = []
|
102
|
+
|
103
|
+
for exc in exception_list:
|
104
|
+
if isinstance(exc, str):
|
105
|
+
try:
|
106
|
+
# Try to dynamically import the exception class
|
107
|
+
exc_class = self._import_exception_class(exc)
|
108
|
+
converted_exceptions.append(exc_class)
|
109
|
+
except (ImportError, AttributeError) as e:
|
110
|
+
warnings.warn(
|
111
|
+
f"Could not import exception class '{exc}': {e}. Using Exception instead.",
|
112
|
+
RuntimeWarning
|
113
|
+
)
|
114
|
+
converted_exceptions.append(Exception)
|
115
|
+
elif isinstance(exc, type) and issubclass(exc, BaseException):
|
116
|
+
converted_exceptions.append(exc)
|
117
|
+
else:
|
118
|
+
warnings.warn(
|
119
|
+
f"Invalid exception type: {type(exc)}. Using Exception instead.",
|
120
|
+
RuntimeWarning
|
121
|
+
)
|
122
|
+
converted_exceptions.append(Exception)
|
123
|
+
|
124
|
+
return converted_exceptions
|
125
|
+
|
126
|
+
def _import_exception_class(self, exception_name: str) -> type:
|
127
|
+
"""Dynamically import an exception class by name.
|
128
|
+
|
129
|
+
Args:
|
130
|
+
exception_name: Name of the exception class to import.
|
131
|
+
|
132
|
+
Returns:
|
133
|
+
The imported exception class.
|
134
|
+
|
135
|
+
Raises:
|
136
|
+
ImportError: If the module cannot be imported.
|
137
|
+
AttributeError: If the exception class is not found in the module.
|
138
|
+
"""
|
139
|
+
# Handle built-in exceptions first
|
140
|
+
built_in_exceptions = {
|
141
|
+
'Exception': Exception,
|
142
|
+
'ValueError': ValueError,
|
143
|
+
'TypeError': TypeError,
|
144
|
+
'RuntimeError': RuntimeError,
|
145
|
+
'FileNotFoundError': FileNotFoundError,
|
146
|
+
'PermissionError': PermissionError,
|
147
|
+
'KeyError': KeyError,
|
148
|
+
'AttributeError': AttributeError,
|
149
|
+
'ImportError': ImportError,
|
150
|
+
'TimeoutError': TimeoutError,
|
151
|
+
}
|
152
|
+
|
153
|
+
if exception_name in built_in_exceptions:
|
154
|
+
return built_in_exceptions[exception_name]
|
155
|
+
|
156
|
+
# Handle module-qualified exceptions (e.g., 'requests.exceptions.HTTPError')
|
157
|
+
if '.' in exception_name:
|
158
|
+
module_name, class_name = exception_name.rsplit('.', 1)
|
159
|
+
module = importlib.import_module(module_name)
|
160
|
+
return getattr(module, class_name)
|
161
|
+
|
162
|
+
# Try to import from common modules
|
163
|
+
common_modules = [
|
164
|
+
'requests.exceptions',
|
165
|
+
'urllib.error',
|
166
|
+
'urllib3.exceptions',
|
167
|
+
'http.client',
|
168
|
+
'socket',
|
169
|
+
'os',
|
170
|
+
'io',
|
171
|
+
]
|
172
|
+
|
173
|
+
for module_name in common_modules:
|
174
|
+
try:
|
175
|
+
module = importlib.import_module(module_name)
|
176
|
+
if hasattr(module, exception_name):
|
177
|
+
return getattr(module, exception_name)
|
178
|
+
except ImportError:
|
179
|
+
continue
|
180
|
+
|
181
|
+
# If not found in common modules, raise an error
|
182
|
+
raise ImportError(f"Could not find exception class: {exception_name}")
|
124
183
|
|
125
184
|
if self.on_failure is not None and not isinstance(self.on_failure, CallbackSpec):
|
126
185
|
if callable(self.on_failure):
|
@@ -1,9 +1,11 @@
|
|
1
1
|
import msgspec
|
2
2
|
from fsspec_utils import AbstractFileSystem, BaseStorageOptions, filesystem
|
3
3
|
import posixpath
|
4
|
+
from typing import Optional
|
4
5
|
|
5
6
|
from ...settings import CONFIG_DIR
|
6
7
|
from ..base import BaseConfig
|
8
|
+
from ..exceptions import ConfigLoadError, ConfigSaveError, ConfigPathError
|
7
9
|
from .adapter import AdapterConfig
|
8
10
|
|
9
11
|
|
@@ -36,6 +38,52 @@ class ProjectConfig(BaseConfig):
|
|
36
38
|
def __post_init__(self):
|
37
39
|
if isinstance(self.adapter, dict):
|
38
40
|
self.adapter = AdapterConfig.from_dict(self.adapter)
|
41
|
+
|
42
|
+
# Validate project name if provided
|
43
|
+
if self.name is not None:
|
44
|
+
self._validate_project_name()
|
45
|
+
|
46
|
+
def _validate_project_name(self) -> None:
|
47
|
+
"""Validate project name parameter.
|
48
|
+
|
49
|
+
Raises:
|
50
|
+
ValueError: If project name contains invalid characters.
|
51
|
+
"""
|
52
|
+
if not isinstance(self.name, str):
|
53
|
+
raise ValueError(f"Project name must be a string, got {type(self.name)}")
|
54
|
+
|
55
|
+
# Check for directory traversal attempts
|
56
|
+
if '..' in self.name or '/' in self.name or '\\' in self.name:
|
57
|
+
raise ValueError(f"Invalid project name: {self.name}. Contains path traversal characters.")
|
58
|
+
|
59
|
+
# Check for empty string
|
60
|
+
if not self.name.strip():
|
61
|
+
raise ValueError("Project name cannot be empty or whitespace only.")
|
62
|
+
|
63
|
+
@classmethod
|
64
|
+
def _load_project_config(cls, fs: AbstractFileSystem, name: str | None) -> "ProjectConfig":
|
65
|
+
"""Centralized project configuration loading logic.
|
66
|
+
|
67
|
+
Args:
|
68
|
+
fs: Filesystem instance.
|
69
|
+
name: Project name.
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
Loaded project configuration.
|
73
|
+
"""
|
74
|
+
if fs.exists("conf/project.yml"):
|
75
|
+
project = cls.from_yaml(path="conf/project.yml", fs=fs)
|
76
|
+
else:
|
77
|
+
project = cls(name=name)
|
78
|
+
return project
|
79
|
+
|
80
|
+
def _save_project_config(self, fs: AbstractFileSystem) -> None:
|
81
|
+
"""Centralized project configuration saving logic.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
fs: Filesystem instance.
|
85
|
+
"""
|
86
|
+
self.to_yaml(path=posixpath.join(CONFIG_DIR, "project.yml"), fs=fs)
|
39
87
|
|
40
88
|
@classmethod
|
41
89
|
def load(
|
@@ -65,15 +113,11 @@ class ProjectConfig(BaseConfig):
|
|
65
113
|
```
|
66
114
|
"""
|
67
115
|
if fs is None:
|
68
|
-
|
69
|
-
|
70
|
-
)
|
71
|
-
|
72
|
-
|
73
|
-
else:
|
74
|
-
project = ProjectConfig(name=name)
|
75
|
-
|
76
|
-
return project
|
116
|
+
# Use cached filesystem for better performance
|
117
|
+
storage_options_hash = cls._hash_storage_options(storage_options)
|
118
|
+
fs = cls._get_cached_filesystem(base_dir, storage_options_hash)
|
119
|
+
|
120
|
+
return cls._load_project_config(fs, name)
|
77
121
|
|
78
122
|
def save(
|
79
123
|
self,
|
@@ -94,12 +138,12 @@ class ProjectConfig(BaseConfig):
|
|
94
138
|
```
|
95
139
|
"""
|
96
140
|
if fs is None:
|
97
|
-
|
98
|
-
|
99
|
-
)
|
141
|
+
# Use cached filesystem for better performance
|
142
|
+
storage_options_hash = self._hash_storage_options(storage_options)
|
143
|
+
fs = self._get_cached_filesystem(base_dir, storage_options_hash)
|
100
144
|
|
101
145
|
fs.makedirs(CONFIG_DIR, exist_ok=True)
|
102
|
-
self.
|
146
|
+
self._save_project_config(fs)
|
103
147
|
|
104
148
|
|
105
149
|
def init_project_config(
|
@@ -136,4 +180,5 @@ def init_project_config(
|
|
136
180
|
storage_options=storage_options,
|
137
181
|
)
|
138
182
|
project.save(base_dir=base_dir, fs=fs, storage_options=storage_options)
|
139
|
-
return project
|
183
|
+
return project
|
184
|
+
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import msgspec
|
2
|
+
import os
|
2
3
|
from munch import munchify
|
3
4
|
|
4
5
|
from ... import settings
|
@@ -11,6 +12,11 @@ class HamiltonTrackerConfig(BaseConfig):
|
|
11
12
|
ui_url: str = msgspec.field(default=settings.HAMILTON_UI_URL)
|
12
13
|
api_key: str | None = msgspec.field(default=None)
|
13
14
|
verify: bool = msgspec.field(default=False)
|
15
|
+
|
16
|
+
def __post_init__(self):
|
17
|
+
# Load API key from environment variable if not explicitly set
|
18
|
+
if self.api_key is None:
|
19
|
+
self.api_key = os.getenv("HAMILTON_API_KEY")
|
14
20
|
|
15
21
|
|
16
22
|
class MLFlowConfig(BaseConfig):
|
flowerpower/cli/__init__.py
CHANGED
@@ -65,7 +65,7 @@ def init(
|
|
65
65
|
parsed_storage_options = (
|
66
66
|
parse_dict_or_list_param(storage_options, "dict") or {}
|
67
67
|
)
|
68
|
-
except
|
68
|
+
except (ValueError, SyntaxError, json.JSONDecodeError) as e:
|
69
69
|
logger.error(f"Error parsing storage options: {e}")
|
70
70
|
raise typer.Exit(code=1)
|
71
71
|
|
@@ -75,8 +75,14 @@ def init(
|
|
75
75
|
base_dir=base_dir,
|
76
76
|
storage_options=parsed_storage_options,
|
77
77
|
)
|
78
|
+
except (FileNotFoundError, PermissionError, OSError) as e:
|
79
|
+
logger.error(f"File system error initializing project: {e}")
|
80
|
+
raise typer.Exit(code=1)
|
81
|
+
except ValueError as e:
|
82
|
+
logger.error(f"Invalid configuration for project: {e}")
|
83
|
+
raise typer.Exit(code=1)
|
78
84
|
except Exception as e:
|
79
|
-
logger.error(f"
|
85
|
+
logger.error(f"Unexpected error initializing project: {e}")
|
80
86
|
raise typer.Exit(code=1)
|
81
87
|
|
82
88
|
|
flowerpower/cli/cfg.py
CHANGED
@@ -1,41 +1,3 @@
|
|
1
1
|
import typer
|
2
2
|
|
3
3
|
app = typer.Typer(help="Config management commands")
|
4
|
-
|
5
|
-
|
6
|
-
# @app.command()
|
7
|
-
# def get_project(request) -> json:
|
8
|
-
# cfg = request.app.ctx.pipeline_manager.cfg.project.to_dict()
|
9
|
-
# # cfg.pop("fs")
|
10
|
-
# return json({"cfg": cfg})
|
11
|
-
|
12
|
-
|
13
|
-
# @bp.get("/pipeline/<pipeline_name>")
|
14
|
-
# async def get_pipeline(request, pipeline_name) -> json:
|
15
|
-
# if pipeline_name != request.app.ctx.pipeline_manager.cfg.pipeline.name:
|
16
|
-
# request.app.ctx.pipeline_manager.load_config(pipeline_name)
|
17
|
-
# cfg = request.app.ctx.pipeline_manager.cfg.pipeline.to_dict()
|
18
|
-
# return json({"cfg": cfg})
|
19
|
-
|
20
|
-
|
21
|
-
# @bp.post("/pipeline/<pipeline_name>")
|
22
|
-
# @openapi.body({"application/json": PipelineConfig}, required=True)
|
23
|
-
# @validate(json=PipelineConfig)
|
24
|
-
# async def update_pipeline(request, pipeline_name, body: PipelineConfig) -> json:
|
25
|
-
# data = request.json
|
26
|
-
# if pipeline_name != request.app.ctx.pipeline_manager.cfg.pipeline.name:
|
27
|
-
# request.app.ctx.pipeline_manager.load_config(pipeline_name)
|
28
|
-
# cfg = request.app.ctx.pipeline_manager.cfg.pipeline.copy()
|
29
|
-
# cfg.update(data)
|
30
|
-
# try:
|
31
|
-
# cfg.to_yaml(
|
32
|
-
# posixpath.join(
|
33
|
-
# "pipelines",
|
34
|
-
# pipeline_name + ".yml",
|
35
|
-
# ),
|
36
|
-
# fs=request.app.ctx.pipeline_manager.cfg.fs,
|
37
|
-
# )
|
38
|
-
# except NotImplementedError as e:
|
39
|
-
# raise SanicException(f"Update failed. {e}", status_code=404)
|
40
|
-
# cfg
|
41
|
-
# return json({"cfg": cfg})
|