FlowerPower 0.21.0__tar.gz → 0.31.0__tar.gz

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 (69) hide show
  1. {flowerpower-0.21.0/src/FlowerPower.egg-info → flowerpower-0.31.0}/PKG-INFO +1 -13
  2. flowerpower-0.31.0/pyproject.toml +142 -0
  3. {flowerpower-0.21.0 → flowerpower-0.31.0/src/FlowerPower.egg-info}/PKG-INFO +1 -13
  4. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/FlowerPower.egg-info/SOURCES.txt +12 -3
  5. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/FlowerPower.egg-info/requires.txt +0 -16
  6. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/cfg/__init__.py +143 -25
  7. flowerpower-0.31.0/src/flowerpower/cfg/base.py +262 -0
  8. flowerpower-0.31.0/src/flowerpower/cfg/exceptions.py +53 -0
  9. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/cfg/pipeline/__init__.py +151 -35
  10. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/cfg/pipeline/adapter.py +1 -0
  11. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/cfg/pipeline/builder.py +24 -25
  12. flowerpower-0.31.0/src/flowerpower/cfg/pipeline/builder_adapter.py +142 -0
  13. flowerpower-0.31.0/src/flowerpower/cfg/pipeline/builder_executor.py +101 -0
  14. flowerpower-0.31.0/src/flowerpower/cfg/pipeline/run.py +195 -0
  15. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/cfg/project/__init__.py +59 -14
  16. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/cfg/project/adapter.py +6 -0
  17. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/cli/__init__.py +8 -9
  18. flowerpower-0.31.0/src/flowerpower/cli/cfg.py +3 -0
  19. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/cli/pipeline.py +121 -83
  20. flowerpower-0.31.0/src/flowerpower/cli/utils.py +197 -0
  21. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/flowerpower.py +94 -120
  22. flowerpower-0.31.0/src/flowerpower/pipeline/config_manager.py +180 -0
  23. flowerpower-0.31.0/src/flowerpower/pipeline/executor.py +126 -0
  24. flowerpower-0.31.0/src/flowerpower/pipeline/lifecycle_manager.py +231 -0
  25. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/pipeline/manager.py +121 -276
  26. flowerpower-0.31.0/src/flowerpower/pipeline/pipeline.py +431 -0
  27. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/pipeline/registry.py +45 -4
  28. flowerpower-0.31.0/src/flowerpower/utils/__init__.py +19 -0
  29. flowerpower-0.31.0/src/flowerpower/utils/adapter.py +286 -0
  30. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/utils/callback.py +73 -67
  31. flowerpower-0.31.0/src/flowerpower/utils/config.py +306 -0
  32. flowerpower-0.31.0/src/flowerpower/utils/executor.py +178 -0
  33. flowerpower-0.31.0/src/flowerpower/utils/filesystem.py +194 -0
  34. flowerpower-0.31.0/src/flowerpower/utils/misc.py +420 -0
  35. flowerpower-0.31.0/src/flowerpower/utils/security.py +221 -0
  36. {flowerpower-0.21.0 → flowerpower-0.31.0}/tests/test_flowerpower_project.py +1 -1
  37. flowerpower-0.21.0/pyproject.toml +0 -81
  38. flowerpower-0.21.0/src/flowerpower/cfg/base.py +0 -141
  39. flowerpower-0.21.0/src/flowerpower/cfg/pipeline/_schedule.py +0 -32
  40. flowerpower-0.21.0/src/flowerpower/cfg/pipeline/run.py +0 -83
  41. flowerpower-0.21.0/src/flowerpower/cli/cfg.py +0 -41
  42. flowerpower-0.21.0/src/flowerpower/cli/mqtt.py +0 -168
  43. flowerpower-0.21.0/src/flowerpower/cli/utils.py +0 -148
  44. flowerpower-0.21.0/src/flowerpower/pipeline/pipeline.py +0 -643
  45. flowerpower-0.21.0/src/flowerpower/plugins/mqtt/__init__.py +0 -8
  46. flowerpower-0.21.0/src/flowerpower/utils/misc.py +0 -247
  47. {flowerpower-0.21.0 → flowerpower-0.31.0}/LICENSE +0 -0
  48. {flowerpower-0.21.0 → flowerpower-0.31.0}/README.md +0 -0
  49. {flowerpower-0.21.0 → flowerpower-0.31.0}/setup.cfg +0 -0
  50. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/FlowerPower.egg-info/dependency_links.txt +0 -0
  51. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/FlowerPower.egg-info/entry_points.txt +0 -0
  52. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/FlowerPower.egg-info/top_level.txt +0 -0
  53. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/__init__.py +0 -0
  54. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/pipeline/__init__.py +0 -0
  55. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/pipeline/base.py +0 -0
  56. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/pipeline/io.py +0 -0
  57. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/pipeline/visualizer.py +0 -0
  58. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/plugins/io/__init__.py +0 -0
  59. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/settings/__init__.py +0 -0
  60. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/settings/_backend.py +0 -0
  61. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/settings/executor.py +0 -0
  62. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/settings/general.py +0 -0
  63. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/settings/hamilton.py +0 -0
  64. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/settings/logging.py +0 -0
  65. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/settings/retry.py +0 -0
  66. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/utils/logging.py +0 -0
  67. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/utils/monkey.py +0 -0
  68. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/utils/open_telemetry.py +0 -0
  69. {flowerpower-0.21.0 → flowerpower-0.31.0}/src/flowerpower/utils/templates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: FlowerPower
3
- Version: 0.21.0
3
+ Version: 0.31.0
4
4
  Summary: A simple workflow framework for building and managing data processing pipelines
5
5
  Author-email: "Volker L." <ligno.blades@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/legout/flowerpower
@@ -25,26 +25,14 @@ Provides-Extra: io
25
25
  Requires-Dist: flowerpower-io>=0.1.1; extra == "io"
26
26
  Provides-Extra: io-legacy
27
27
  Requires-Dist: flowerpower-io[legacy]>=0.1.1; extra == "io-legacy"
28
- Provides-Extra: mongodb
29
- Requires-Dist: pymongo>=4.7.2; extra == "mongodb"
30
- Provides-Extra: mqtt
31
- Requires-Dist: paho-mqtt>=2.1.0; extra == "mqtt"
32
- Requires-Dist: orjson>=3.10.11; extra == "mqtt"
33
- Requires-Dist: mmh3>=5.1.0; extra == "mqtt"
34
28
  Provides-Extra: opentelemetry
35
29
  Requires-Dist: opentelemetry-api>=1.5.0; extra == "opentelemetry"
36
30
  Requires-Dist: opentelemetry-sdk>=1.5.0; extra == "opentelemetry"
37
31
  Requires-Dist: opentelemetry-exporter-jaeger>=1.21.0; extra == "opentelemetry"
38
32
  Provides-Extra: ray
39
33
  Requires-Dist: ray>=2.34.0; extra == "ray"
40
- Provides-Extra: tui
41
- Requires-Dist: textual>=0.85.2; extra == "tui"
42
34
  Provides-Extra: ui
43
35
  Requires-Dist: sf-hamilton-ui>=0.0.11; extra == "ui"
44
- Provides-Extra: webserver
45
- Requires-Dist: sanic>=24.6.0; extra == "webserver"
46
- Requires-Dist: sanic-ext>=23.12.0; extra == "webserver"
47
- Requires-Dist: orjson>=3.10.11; extra == "webserver"
48
36
  Provides-Extra: openlineage
49
37
  Requires-Dist: openlineage-python>=1.32.0; extra == "openlineage"
50
38
  Dynamic: license-file
@@ -0,0 +1,142 @@
1
+ [project]
2
+ name = "FlowerPower"
3
+ description = "A simple workflow framework for building and managing data processing pipelines"
4
+ authors = [{ name = "Volker L.", email = "ligno.blades@gmail.com" }]
5
+ readme = "README.md"
6
+ requires-python = ">= 3.11"
7
+ version = "0.31.0"
8
+ keywords = ["hamilton", "workflow", "pipeline", "scheduler", "dask", "ray"]
9
+
10
+ dependencies = [
11
+ #'dill>=0.3.8',
12
+ 'duration-parser>=1.0.1',
13
+ 'fsspec>=2024.10.0',
14
+ 'fsspec-utils>=0.1.0',
15
+ 'humanize>=4.12.2',
16
+ 'msgspec>=0.19.0',
17
+ 'munch>=4.0.0',
18
+ #"openai>=1.100.2",
19
+ #'orjson>=3.10.15',
20
+ #'python-dotenv>=1.0.1',
21
+ 'pyyaml>=6.0.1',
22
+ 'rich>=13.9.3',
23
+ 's3fs>=2024.10.0',
24
+ 'sf-hamilton-sdk>=0.5.2',
25
+ 'sf-hamilton[visualization,rich,tqdm]>=1.69.0',
26
+ 'typer>=0.12.3',
27
+ ]
28
+
29
+
30
+ [project.urls]
31
+ "Homepage" = "https://github.com/legout/flowerpower"
32
+ "Bug Tracker" = "https://github.com/legout/flowerpower/issues"
33
+
34
+ [project.scripts]
35
+ flowerpower = "flowerpower.cli:app"
36
+
37
+ [project.optional-dependencies]
38
+ io = ["flowerpower-io>=0.1.1"]
39
+ io-legacy = ["flowerpower-io[legacy]>=0.1.1"]
40
+ opentelemetry = [
41
+ "opentelemetry-api>=1.5.0",
42
+ "opentelemetry-sdk>=1.5.0",
43
+ "opentelemetry-exporter-jaeger>=1.21.0", #"sf-hamilton[opentelemetry]>=1.83.3"
44
+ ]
45
+ ray = ["ray>=2.34.0"]
46
+ ui = ["sf-hamilton-ui>=0.0.11"]
47
+
48
+ openlineage = ["openlineage-python>=1.32.0"]
49
+
50
+
51
+ [tool.uv]
52
+ dev-dependencies = [
53
+ "ipython>=8.24.0",
54
+ "isort>=5.13.2",
55
+ "ruff>=0.7.1",
56
+ "jupyterlab>=4.3.0",
57
+ "pytest>=8.3.4",
58
+ "pytest-mock>=3.12.0",
59
+ "pytest-cov>=4.1.0",
60
+ "marimo>=0.10.19",
61
+ "pre-commit>=4.2.0",
62
+ "mkdocs>=1.6.1",
63
+ "mkdocs-material>=9.6.17",
64
+ "quarto>=0.1.0",
65
+ "mkdocs-glightbox>=0.4.0",
66
+ "mkdocs-mermaid2-plugin>=1.2.1",
67
+ "pymdown-extensions>=10.16.1",
68
+ "mkdocstrings>=0.30.0",
69
+ "mkdocstrings-python>=1.17.0",
70
+ "repomix>=0.3.4",
71
+ # Security audit tools
72
+ "bandit[toml]>=1.7.7",
73
+ "safety>=3.2.0",
74
+ "mypy>=1.13.0",
75
+ "pandas>=2.3.2",
76
+ "numpy>=2.3.3",
77
+ "matplotlib>=3.10.6",
78
+ "seaborn>=0.13.2",
79
+ ]
80
+ package = true
81
+
82
+ # Security configuration
83
+ [tool.bandit]
84
+ exclude_dirs = ["tests", "examples"]
85
+ skips = ["B101"] # Skip assert_used test for test files
86
+
87
+ [tool.bandit.assert_used]
88
+ skips = ["*/test_*.py", "*/tests.py"]
89
+
90
+ # MyPy configuration
91
+ [tool.mypy]
92
+ python_version = "3.11"
93
+ warn_return_any = true
94
+ warn_unused_configs = true
95
+ disallow_untyped_defs = true
96
+ disallow_incomplete_defs = true
97
+ check_untyped_defs = true
98
+ disallow_untyped_decorators = true
99
+ no_implicit_optional = true
100
+ warn_redundant_casts = true
101
+ warn_unused_ignores = true
102
+ warn_no_return = true
103
+ warn_unreachable = true
104
+ strict_equality = true
105
+ show_error_codes = true
106
+
107
+ [[tool.mypy.overrides]]
108
+ module = [
109
+ "hamilton.*",
110
+ "sf_hamilton.*",
111
+ "fsspec_utils.*",
112
+ "loguru.*",
113
+ "munch.*",
114
+ "rich.*",
115
+ "typer.*",
116
+ ]
117
+ ignore_missing_imports = true
118
+
119
+ # Ruff configuration (extended for security)
120
+ [tool.ruff]
121
+ line-length = 88
122
+ target-version = "py311"
123
+
124
+ [tool.ruff.lint]
125
+ select = [
126
+ "E", # pycodestyle errors
127
+ "W", # pycodestyle warnings
128
+ "F", # pyflakes
129
+ "I", # isort
130
+ "B", # flake8-bugbear
131
+ "C4", # flake8-comprehensions
132
+ "UP", # pyupgrade
133
+ "S", # flake8-bandit (security)
134
+ ]
135
+ ignore = [
136
+ "E501", # line too long, handled by black
137
+ "B008", # do not perform function calls in argument defaults
138
+ ]
139
+
140
+ [tool.ruff.lint.per-file-ignores]
141
+ "tests/*" = ["S101"] # Allow assert in tests
142
+ "examples/*" = ["S101"] # Allow assert in examples
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: FlowerPower
3
- Version: 0.21.0
3
+ Version: 0.31.0
4
4
  Summary: A simple workflow framework for building and managing data processing pipelines
5
5
  Author-email: "Volker L." <ligno.blades@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/legout/flowerpower
@@ -25,26 +25,14 @@ Provides-Extra: io
25
25
  Requires-Dist: flowerpower-io>=0.1.1; extra == "io"
26
26
  Provides-Extra: io-legacy
27
27
  Requires-Dist: flowerpower-io[legacy]>=0.1.1; extra == "io-legacy"
28
- Provides-Extra: mongodb
29
- Requires-Dist: pymongo>=4.7.2; extra == "mongodb"
30
- Provides-Extra: mqtt
31
- Requires-Dist: paho-mqtt>=2.1.0; extra == "mqtt"
32
- Requires-Dist: orjson>=3.10.11; extra == "mqtt"
33
- Requires-Dist: mmh3>=5.1.0; extra == "mqtt"
34
28
  Provides-Extra: opentelemetry
35
29
  Requires-Dist: opentelemetry-api>=1.5.0; extra == "opentelemetry"
36
30
  Requires-Dist: opentelemetry-sdk>=1.5.0; extra == "opentelemetry"
37
31
  Requires-Dist: opentelemetry-exporter-jaeger>=1.21.0; extra == "opentelemetry"
38
32
  Provides-Extra: ray
39
33
  Requires-Dist: ray>=2.34.0; extra == "ray"
40
- Provides-Extra: tui
41
- Requires-Dist: textual>=0.85.2; extra == "tui"
42
34
  Provides-Extra: ui
43
35
  Requires-Dist: sf-hamilton-ui>=0.0.11; extra == "ui"
44
- Provides-Extra: webserver
45
- Requires-Dist: sanic>=24.6.0; extra == "webserver"
46
- Requires-Dist: sanic-ext>=23.12.0; extra == "webserver"
47
- Requires-Dist: orjson>=3.10.11; extra == "webserver"
48
36
  Provides-Extra: openlineage
49
37
  Requires-Dist: openlineage-python>=1.32.0; extra == "openlineage"
50
38
  Dynamic: license-file
@@ -11,27 +11,30 @@ src/flowerpower/__init__.py
11
11
  src/flowerpower/flowerpower.py
12
12
  src/flowerpower/cfg/__init__.py
13
13
  src/flowerpower/cfg/base.py
14
+ src/flowerpower/cfg/exceptions.py
14
15
  src/flowerpower/cfg/pipeline/__init__.py
15
- src/flowerpower/cfg/pipeline/_schedule.py
16
16
  src/flowerpower/cfg/pipeline/adapter.py
17
17
  src/flowerpower/cfg/pipeline/builder.py
18
+ src/flowerpower/cfg/pipeline/builder_adapter.py
19
+ src/flowerpower/cfg/pipeline/builder_executor.py
18
20
  src/flowerpower/cfg/pipeline/run.py
19
21
  src/flowerpower/cfg/project/__init__.py
20
22
  src/flowerpower/cfg/project/adapter.py
21
23
  src/flowerpower/cli/__init__.py
22
24
  src/flowerpower/cli/cfg.py
23
- src/flowerpower/cli/mqtt.py
24
25
  src/flowerpower/cli/pipeline.py
25
26
  src/flowerpower/cli/utils.py
26
27
  src/flowerpower/pipeline/__init__.py
27
28
  src/flowerpower/pipeline/base.py
29
+ src/flowerpower/pipeline/config_manager.py
30
+ src/flowerpower/pipeline/executor.py
28
31
  src/flowerpower/pipeline/io.py
32
+ src/flowerpower/pipeline/lifecycle_manager.py
29
33
  src/flowerpower/pipeline/manager.py
30
34
  src/flowerpower/pipeline/pipeline.py
31
35
  src/flowerpower/pipeline/registry.py
32
36
  src/flowerpower/pipeline/visualizer.py
33
37
  src/flowerpower/plugins/io/__init__.py
34
- src/flowerpower/plugins/mqtt/__init__.py
35
38
  src/flowerpower/settings/__init__.py
36
39
  src/flowerpower/settings/_backend.py
37
40
  src/flowerpower/settings/executor.py
@@ -39,10 +42,16 @@ src/flowerpower/settings/general.py
39
42
  src/flowerpower/settings/hamilton.py
40
43
  src/flowerpower/settings/logging.py
41
44
  src/flowerpower/settings/retry.py
45
+ src/flowerpower/utils/__init__.py
46
+ src/flowerpower/utils/adapter.py
42
47
  src/flowerpower/utils/callback.py
48
+ src/flowerpower/utils/config.py
49
+ src/flowerpower/utils/executor.py
50
+ src/flowerpower/utils/filesystem.py
43
51
  src/flowerpower/utils/logging.py
44
52
  src/flowerpower/utils/misc.py
45
53
  src/flowerpower/utils/monkey.py
46
54
  src/flowerpower/utils/open_telemetry.py
55
+ src/flowerpower/utils/security.py
47
56
  src/flowerpower/utils/templates.py
48
57
  tests/test_flowerpower_project.py
@@ -17,14 +17,6 @@ flowerpower-io>=0.1.1
17
17
  [io-legacy]
18
18
  flowerpower-io[legacy]>=0.1.1
19
19
 
20
- [mongodb]
21
- pymongo>=4.7.2
22
-
23
- [mqtt]
24
- paho-mqtt>=2.1.0
25
- orjson>=3.10.11
26
- mmh3>=5.1.0
27
-
28
20
  [openlineage]
29
21
  openlineage-python>=1.32.0
30
22
 
@@ -36,13 +28,5 @@ opentelemetry-exporter-jaeger>=1.21.0
36
28
  [ray]
37
29
  ray>=2.34.0
38
30
 
39
- [tui]
40
- textual>=0.85.2
41
-
42
31
  [ui]
43
32
  sf-hamilton-ui>=0.0.11
44
-
45
- [webserver]
46
- sanic>=24.6.0
47
- sanic-ext>=23.12.0
48
- orjson>=3.10.11
@@ -6,6 +6,7 @@ from munch import Munch
6
6
 
7
7
  from ..settings import CONFIG_DIR, PIPELINES_DIR
8
8
  from .base import BaseConfig
9
+ from .exceptions import ConfigLoadError, ConfigSaveError, ConfigPathError
9
10
  from .pipeline import PipelineConfig, init_pipeline_config
10
11
  from .project import ProjectConfig, init_project_config
11
12
 
@@ -21,8 +22,9 @@ class Config(BaseConfig):
21
22
  pipeline (PipelineConfig): Configuration for the pipeline.
22
23
  project (ProjectConfig): Configuration for the project.
23
24
  fs (AbstractFileSystem | None): Filesystem abstraction for I/O operations.
24
- base_dir (str | Path | None): Base directory for the configuration.
25
- storage_options (dict | Munch): Options for filesystem operations.
25
+ base_dir (str | None): Base directory for the configuration.
26
+ base_dir_path (pathlib.Path | None): Base directory as a Path object (property).
27
+ storage_options (Munch): Options for filesystem operations.
26
28
 
27
29
  Example:
28
30
  ```python
@@ -41,8 +43,61 @@ class Config(BaseConfig):
41
43
  pipeline: PipelineConfig = msgspec.field(default_factory=PipelineConfig)
42
44
  project: ProjectConfig = msgspec.field(default_factory=ProjectConfig)
43
45
  fs: AbstractFileSystem | None = None
44
- base_dir: str | Path | None = None
45
- storage_options: dict | Munch = msgspec.field(default_factory=Munch)
46
+ base_dir: str | None = None
47
+ storage_options: Munch = msgspec.field(default_factory=Munch)
48
+
49
+ def __post_init__(self):
50
+ """Handle conversion of storage_options from dict to Munch if needed."""
51
+ if isinstance(self.storage_options, dict):
52
+ self.storage_options = Munch(self.storage_options)
53
+
54
+ # Validate storage_options
55
+ self._validate_storage_options()
56
+
57
+ # Validate base_dir if provided
58
+ if self.base_dir is not None:
59
+ self._validate_base_dir()
60
+
61
+ def _validate_storage_options(self) -> None:
62
+ """Validate storage_options parameter.
63
+
64
+ Raises:
65
+ ValueError: If storage_options contains invalid values.
66
+ """
67
+ if self.storage_options is None:
68
+ self.storage_options = Munch()
69
+
70
+ if not isinstance(self.storage_options, (dict, Munch)):
71
+ raise ValueError(f"storage_options must be a dict or Munch, got {type(self.storage_options)}")
72
+
73
+ def _validate_base_dir(self) -> None:
74
+ """Validate base_dir parameter.
75
+
76
+ Raises:
77
+ ValueError: If base_dir contains invalid characters or is empty.
78
+ """
79
+ # Convert Path to string if needed
80
+ base_dir_str = str(self.base_dir) if hasattr(self.base_dir, '__str__') else self.base_dir
81
+
82
+ if not isinstance(base_dir_str, str):
83
+ raise ValueError(f"base_dir must be a string or Path, got {type(self.base_dir)}")
84
+
85
+ # Check for directory traversal attempts (but allow absolute paths)
86
+ if '..' in base_dir_str:
87
+ raise ValueError(f"Invalid base_dir: {base_dir_str}. Contains path traversal characters.")
88
+
89
+ # Check for empty string
90
+ if not base_dir_str.strip():
91
+ raise ValueError("base_dir cannot be empty or whitespace only.")
92
+
93
+ @property
94
+ def base_dir_path(self) -> Path | None:
95
+ """Get base_dir as a pathlib.Path object.
96
+
97
+ Returns:
98
+ pathlib.Path | None: The base directory as a Path object, or None if base_dir is None.
99
+ """
100
+ return Path(self.base_dir) if self.base_dir is not None else None
46
101
 
47
102
  @classmethod
48
103
  def load(
@@ -75,21 +130,29 @@ class Config(BaseConfig):
75
130
  ```
76
131
  """
77
132
  if fs is None:
78
- fs = filesystem(
79
- base_dir, cached=True, dirfs=True, storage_options=storage_options
133
+ # Use cached filesystem for better performance
134
+ storage_options_hash = cls._hash_storage_options(storage_options)
135
+ fs = cls._get_cached_filesystem(base_dir, storage_options_hash)
136
+
137
+ try:
138
+ project = ProjectConfig.load(
139
+ base_dir=base_dir,
140
+ name=name,
141
+ fs=fs,
142
+ storage_options=storage_options,
80
143
  )
81
- project = ProjectConfig.load(
82
- base_dir=base_dir,
83
- name=name,
84
- fs=fs,
85
- storage_options=storage_options,
86
- )
87
- pipeline = PipelineConfig.load(
88
- base_dir=base_dir,
89
- name=pipeline_name,
90
- fs=fs,
91
- storage_options=storage_options,
92
- )
144
+ except ConfigLoadError as e:
145
+ raise ConfigLoadError(f"Failed to load project configuration: {e}", path=base_dir, original_error=e)
146
+
147
+ try:
148
+ pipeline = PipelineConfig.load(
149
+ base_dir=base_dir,
150
+ name=pipeline_name,
151
+ fs=fs,
152
+ storage_options=storage_options,
153
+ )
154
+ except ConfigLoadError as e:
155
+ raise ConfigLoadError(f"Failed to load pipeline configuration: {e}", path=base_dir, original_error=e)
93
156
 
94
157
  return cls(
95
158
  base_dir=base_dir,
@@ -120,9 +183,9 @@ class Config(BaseConfig):
120
183
  ```
121
184
  """
122
185
  if fs is None and self.fs is None:
123
- self.fs = filesystem(
124
- self.base_dir, cached=True, dirfs=True, **storage_options
125
- )
186
+ # Use cached filesystem for better performance
187
+ storage_options_hash = self._hash_storage_options(storage_options)
188
+ self.fs = self._get_cached_filesystem(self.base_dir, storage_options_hash)
126
189
 
127
190
  if not self.fs.exists(CONFIG_DIR):
128
191
  self.fs.makedirs(CONFIG_DIR)
@@ -130,13 +193,22 @@ class Config(BaseConfig):
130
193
  if pipeline:
131
194
  self.fs.makedirs(PIPELINES_DIR, exist_ok=True)
132
195
  h_params = self.pipeline.pop("h_params") if self.pipeline.h_params else None
133
- self.pipeline.to_yaml(
134
- path=f"conf/pipelines/{self.pipeline.name}.yml", fs=self.fs
135
- )
196
+ # Validate pipeline name to prevent directory traversal
197
+ if self.pipeline.name and ('..' in self.pipeline.name or '/' in self.pipeline.name or '\\' in self.pipeline.name):
198
+ raise ConfigPathError(f"Invalid pipeline name: {self.pipeline.name}. Contains path traversal characters.", path=self.pipeline.name)
199
+ try:
200
+ self.pipeline.to_yaml(
201
+ path=f"conf/pipelines/{self.pipeline.name}.yml", fs=self.fs
202
+ )
203
+ except ConfigSaveError as e:
204
+ raise ConfigSaveError(f"Failed to save pipeline configuration: {e}", path=f"conf/pipelines/{self.pipeline.name}.yml", original_error=e)
136
205
  if h_params:
137
206
  self.pipeline.h_params = h_params
138
207
  if project:
139
- self.project.to_yaml("conf/project.yml", self.fs)
208
+ try:
209
+ self.project.to_yaml("conf/project.yml", self.fs)
210
+ except ConfigSaveError as e:
211
+ raise ConfigSaveError(f"Failed to save project configuration: {e}", path="conf/project.yml", original_error=e)
140
212
 
141
213
 
142
214
  def load(
@@ -247,3 +319,49 @@ def init_config(
247
319
  storage_options=storage_options,
248
320
  )
249
321
  return Config(pipeline=pipeline_cfg, project=project_cfg, fs=fs, base_dir=base_dir)
322
+
323
+
324
+ # Helper methods for centralized load/save logic
325
+ @classmethod
326
+ def _load_config(
327
+ cls,
328
+ config_class: type[BaseConfig],
329
+ base_dir: str,
330
+ name: str | None,
331
+ fs: AbstractFileSystem,
332
+ storage_options: dict | BaseStorageOptions | None,
333
+ ) -> BaseConfig:
334
+ """Centralized configuration loading logic.
335
+
336
+ Args:
337
+ config_class: The configuration class to load.
338
+ base_dir: Base directory for configurations.
339
+ name: Configuration name.
340
+ fs: Filesystem instance.
341
+ storage_options: Options for filesystem.
342
+
343
+ Returns:
344
+ Loaded configuration instance.
345
+ """
346
+ return config_class.load(
347
+ base_dir=base_dir,
348
+ name=name,
349
+ fs=fs,
350
+ storage_options=storage_options,
351
+ )
352
+
353
+
354
+ def _save_pipeline_config(self) -> None:
355
+ """Save pipeline configuration with proper handling of h_params."""
356
+ self.fs.makedirs(PIPELINES_DIR, exist_ok=True)
357
+ h_params = self.pipeline.pop("h_params") if self.pipeline.h_params else None
358
+ self.pipeline.to_yaml(
359
+ path=f"conf/pipelines/{self.pipeline.name}.yml", fs=self.fs
360
+ )
361
+ if h_params:
362
+ self.pipeline.h_params = h_params
363
+
364
+
365
+ def _save_project_config(self) -> None:
366
+ """Save project configuration."""
367
+ self.project.to_yaml("conf/project.yml", self.fs)