mcp-souschef 2.1.2__py3-none-any.whl → 2.2.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.
@@ -0,0 +1,275 @@
1
+ """Enhanced error handling with actionable messages and recovery suggestions."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ class SousChefError(Exception):
7
+ """Base exception for SousChef with enhanced error messages."""
8
+
9
+ def __init__(self, message: str, suggestion: str | None = None):
10
+ """
11
+ Initialize with message and optional recovery suggestion.
12
+
13
+ Args:
14
+ message: The error message describing what went wrong.
15
+ suggestion: Optional suggestion for how to fix the error.
16
+
17
+ """
18
+ self.message = message
19
+ self.suggestion = suggestion
20
+ full_message = message
21
+ if suggestion:
22
+ full_message = f"{message}\n\nSuggestion: {suggestion}"
23
+ super().__init__(full_message)
24
+
25
+
26
+ class ChefFileNotFoundError(SousChefError):
27
+ """Raised when a required file cannot be found."""
28
+
29
+ def __init__(self, path: str, file_type: str = "file"):
30
+ """
31
+ Initialize file not found error.
32
+
33
+ Args:
34
+ path: The path that was not found.
35
+ file_type: Type of file (e.g., 'cookbook', 'recipe', 'template').
36
+
37
+ """
38
+ message = f"Could not find {file_type}: {path}"
39
+ suggestion = (
40
+ "Check that the path exists and you have read permissions. "
41
+ "For cookbooks, ensure you're pointing to the cookbook root "
42
+ "directory containing metadata.rb."
43
+ )
44
+ super().__init__(message, suggestion)
45
+
46
+
47
+ class InvalidCookbookError(SousChefError):
48
+ """Raised when a cookbook is invalid or malformed."""
49
+
50
+ def __init__(self, path: str, reason: str):
51
+ """
52
+ Initialize invalid cookbook error.
53
+
54
+ Args:
55
+ path: The cookbook path.
56
+ reason: Why the cookbook is invalid.
57
+
58
+ """
59
+ message = f"Invalid cookbook at {path}: {reason}"
60
+ suggestion = (
61
+ "Ensure the directory contains a valid Chef cookbook with "
62
+ "metadata.rb. Run 'knife cookbook test' to validate the "
63
+ "cookbook structure."
64
+ )
65
+ super().__init__(message, suggestion)
66
+
67
+
68
+ class ParseError(SousChefError):
69
+ """Raised when parsing Chef code fails."""
70
+
71
+ def __init__(
72
+ self, file_path: str, line_number: int | None = None, detail: str = ""
73
+ ):
74
+ """
75
+ Initialize parse error.
76
+
77
+ Args:
78
+ file_path: The file that failed to parse.
79
+ line_number: Optional line number where parsing failed.
80
+ detail: Additional detail about the parse failure.
81
+
82
+ """
83
+ location = f" at line {line_number}" if line_number else ""
84
+ message = f"Failed to parse {file_path}{location}"
85
+ if detail:
86
+ message += f": {detail}"
87
+ suggestion = (
88
+ "Check that the file contains valid Chef Ruby DSL syntax. "
89
+ "Complex Ruby code may require manual review."
90
+ )
91
+ super().__init__(message, suggestion)
92
+
93
+
94
+ class ConversionError(SousChefError):
95
+ """Raised when conversion from Chef to Ansible fails."""
96
+
97
+ def __init__(self, resource_type: str, reason: str):
98
+ """
99
+ Initialize conversion error.
100
+
101
+ Args:
102
+ resource_type: The Chef resource type that failed to convert.
103
+ reason: Why the conversion failed.
104
+
105
+ """
106
+ message = f"Cannot convert Chef resource '{resource_type}': {reason}"
107
+ suggestion = (
108
+ "This resource may require manual conversion. Check the Ansible "
109
+ "module documentation for equivalent modules, or consider using "
110
+ "the 'command' or 'shell' module as a fallback."
111
+ )
112
+ super().__init__(message, suggestion)
113
+
114
+
115
+ class ValidationError(SousChefError):
116
+ """Raised when validation of converted content fails."""
117
+
118
+ def __init__(self, validation_type: str, issues: list[str]):
119
+ """
120
+ Initialize validation error.
121
+
122
+ Args:
123
+ validation_type: Type of validation that failed.
124
+ issues: List of validation issues found.
125
+
126
+ """
127
+ issue_list = "\n - ".join(issues)
128
+ message = f"{validation_type} validation failed:\n - {issue_list}"
129
+ suggestion = (
130
+ "Review the validation issues above and fix them in the "
131
+ "generated output. Run the validation again after making "
132
+ "corrections."
133
+ )
134
+ super().__init__(message, suggestion)
135
+
136
+
137
+ def validate_file_exists(path: str, file_type: str = "file") -> Path:
138
+ """
139
+ Validate that a file exists and is readable.
140
+
141
+ Args:
142
+ path: Path to validate.
143
+ file_type: Type of file for error messages.
144
+
145
+ Returns:
146
+ Path object if validation succeeds.
147
+
148
+ Raises:
149
+ FileNotFoundError: If file doesn't exist or isn't readable.
150
+
151
+ """
152
+ file_path = Path(path)
153
+ if not file_path.exists():
154
+ raise ChefFileNotFoundError(path, file_type)
155
+ if not file_path.is_file():
156
+ raise ChefFileNotFoundError(path, file_type)
157
+ try:
158
+ # Test readability
159
+ with file_path.open() as f:
160
+ f.read(1)
161
+ except PermissionError as e:
162
+ raise SousChefError(
163
+ f"Permission denied reading {file_type}: {path}",
164
+ "Ensure you have read permissions on the file. On Unix systems, "
165
+ "try 'chmod +r' on the file.",
166
+ ) from e
167
+ return file_path
168
+
169
+
170
+ def validate_directory_exists(path: str, dir_type: str = "directory") -> Path:
171
+ """
172
+ Validate that a directory exists and is readable.
173
+
174
+ Args:
175
+ path: Path to validate.
176
+ dir_type: Type of directory for error messages.
177
+
178
+ Returns:
179
+ Path object if validation succeeds.
180
+
181
+ Raises:
182
+ FileNotFoundError: If directory doesn't exist or isn't readable.
183
+
184
+ """
185
+ dir_path = Path(path)
186
+ if not dir_path.exists():
187
+ raise ChefFileNotFoundError(path, dir_type)
188
+ if not dir_path.is_dir():
189
+ raise SousChefError(
190
+ f"Path is not a {dir_type}: {path}",
191
+ f"Expected a directory but found a file. Check that you're "
192
+ f"pointing to the {dir_type} directory, not a file within it.",
193
+ )
194
+ try:
195
+ # Test readability
196
+ list(dir_path.iterdir())
197
+ except PermissionError as e:
198
+ raise SousChefError(
199
+ f"Permission denied reading {dir_type}: {path}",
200
+ "Ensure you have read and execute permissions on the directory. "
201
+ "On Unix systems, try 'chmod +rx' on the directory.",
202
+ ) from e
203
+ return dir_path
204
+
205
+
206
+ def validate_cookbook_structure(path: str) -> Path:
207
+ """
208
+ Validate that a path contains a valid Chef cookbook.
209
+
210
+ Args:
211
+ path: Path to the cookbook root directory.
212
+
213
+ Returns:
214
+ Path object if validation succeeds.
215
+
216
+ Raises:
217
+ InvalidCookbookError: If the directory isn't a valid cookbook.
218
+
219
+ """
220
+ cookbook_path = validate_directory_exists(path, "cookbook")
221
+
222
+ # Check for metadata.rb or metadata.json
223
+ has_metadata = (cookbook_path / "metadata.rb").exists() or (
224
+ cookbook_path / "metadata.json"
225
+ ).exists()
226
+
227
+ if not has_metadata:
228
+ raise InvalidCookbookError(
229
+ path, "No metadata.rb or metadata.json found in cookbook root"
230
+ )
231
+
232
+ return cookbook_path
233
+
234
+
235
+ def format_error_with_context(
236
+ error: Exception, operation: str, file_path: str | None = None
237
+ ) -> str:
238
+ """
239
+ Format an error message with operation context.
240
+
241
+ Args:
242
+ error: The exception that occurred.
243
+ operation: Description of the operation that failed.
244
+ file_path: Optional path to the file being processed.
245
+
246
+ Returns:
247
+ Formatted error message with context and suggestions.
248
+
249
+ """
250
+ if isinstance(error, SousChefError):
251
+ # Already has good formatting
252
+ return str(error)
253
+
254
+ context = f"Error during {operation}"
255
+ if file_path:
256
+ context += f" for {file_path}"
257
+
258
+ if isinstance(error, FileNotFoundError):
259
+ return str(ChefFileNotFoundError(file_path or "unknown", "file"))
260
+ elif isinstance(error, PermissionError):
261
+ return (
262
+ f"{context}: Permission denied\n\nSuggestion: Check "
263
+ "file/directory permissions and ensure you have read access."
264
+ )
265
+ elif isinstance(error, (ValueError, TypeError)):
266
+ return (
267
+ f"{context}: {error}\n\nSuggestion: Check that input "
268
+ "values are in the correct format and type."
269
+ )
270
+ else:
271
+ return (
272
+ f"{context}: {error}\n\nSuggestion: If this error persists, "
273
+ "please report it with the full error message at "
274
+ "https://github.com/kpeacocke/souschef/issues"
275
+ )