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
@@ -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
- self.inputs = munchify(self.inputs)
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
- converted_exceptions = []
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
- fs = filesystem(
69
- base_dir, cached=False, dirfs=True, storage_options=storage_options
70
- )
71
- if fs.exists("conf/project.yml"):
72
- project = ProjectConfig.from_yaml(path="conf/project.yml", fs=fs)
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
- fs = filesystem(
98
- base_dir, cached=True, dirfs=True, storage_options=storage_options
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.to_yaml(path=posixpath.join(CONFIG_DIR, "project.yml"), fs=fs)
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):
@@ -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 Exception as e:
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"Error initializing project: {e}")
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})