claude-mpm 4.4.0__py3-none-any.whl → 4.4.3__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 (50) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/WORKFLOW.md +2 -14
  3. claude_mpm/cli/commands/configure.py +2 -29
  4. claude_mpm/cli/commands/mpm_init.py +3 -3
  5. claude_mpm/cli/parsers/configure_parser.py +4 -15
  6. claude_mpm/core/framework/__init__.py +38 -0
  7. claude_mpm/core/framework/formatters/__init__.py +11 -0
  8. claude_mpm/core/framework/formatters/capability_generator.py +356 -0
  9. claude_mpm/core/framework/formatters/content_formatter.py +283 -0
  10. claude_mpm/core/framework/formatters/context_generator.py +180 -0
  11. claude_mpm/core/framework/loaders/__init__.py +13 -0
  12. claude_mpm/core/framework/loaders/agent_loader.py +202 -0
  13. claude_mpm/core/framework/loaders/file_loader.py +213 -0
  14. claude_mpm/core/framework/loaders/instruction_loader.py +151 -0
  15. claude_mpm/core/framework/loaders/packaged_loader.py +208 -0
  16. claude_mpm/core/framework/processors/__init__.py +11 -0
  17. claude_mpm/core/framework/processors/memory_processor.py +222 -0
  18. claude_mpm/core/framework/processors/metadata_processor.py +146 -0
  19. claude_mpm/core/framework/processors/template_processor.py +238 -0
  20. claude_mpm/core/framework_loader.py +277 -1798
  21. claude_mpm/hooks/__init__.py +9 -1
  22. claude_mpm/hooks/kuzu_memory_hook.py +352 -0
  23. claude_mpm/services/core/path_resolver.py +1 -0
  24. claude_mpm/services/diagnostics/diagnostic_runner.py +1 -0
  25. claude_mpm/services/mcp_config_manager.py +67 -4
  26. claude_mpm/services/mcp_gateway/core/process_pool.py +281 -0
  27. claude_mpm/services/mcp_gateway/core/startup_verification.py +2 -2
  28. claude_mpm/services/mcp_gateway/main.py +3 -13
  29. claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -10
  30. claude_mpm/services/mcp_gateway/tools/__init__.py +13 -2
  31. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +36 -6
  32. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +542 -0
  33. claude_mpm/services/shared/__init__.py +2 -1
  34. claude_mpm/services/shared/service_factory.py +8 -5
  35. claude_mpm/services/unified/config_strategies/__init__.py +190 -0
  36. claude_mpm/services/unified/config_strategies/config_schema.py +689 -0
  37. claude_mpm/services/unified/config_strategies/context_strategy.py +748 -0
  38. claude_mpm/services/unified/config_strategies/error_handling_strategy.py +999 -0
  39. claude_mpm/services/unified/config_strategies/file_loader_strategy.py +871 -0
  40. claude_mpm/services/unified/config_strategies/unified_config_service.py +802 -0
  41. claude_mpm/services/unified/config_strategies/validation_strategy.py +1105 -0
  42. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.3.dist-info}/METADATA +15 -15
  43. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.3.dist-info}/RECORD +47 -27
  44. claude_mpm/cli/commands/configure_tui.py +0 -1927
  45. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +0 -645
  46. claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +0 -602
  47. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.3.dist-info}/WHEEL +0 -0
  48. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.3.dist-info}/entry_points.txt +0 -0
  49. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.3.dist-info}/licenses/LICENSE +0 -0
  50. {claude_mpm-4.4.0.dist-info → claude_mpm-4.4.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,999 @@
1
+ """
2
+ Error Handling Strategy - Unifies 99 error handling patterns into composable handlers
3
+ Part of Phase 3 Configuration Consolidation
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+ from typing import Any, Dict, List, Optional, Union, Callable, Type
8
+ from dataclasses import dataclass, field
9
+ from enum import Enum
10
+ import traceback
11
+ import sys
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ import json
15
+
16
+ from claude_mpm.core.logging_utils import get_logger
17
+ from .unified_config_service import IConfigStrategy
18
+
19
+
20
+ class ErrorSeverity(Enum):
21
+ """Error severity levels"""
22
+ CRITICAL = "critical" # System failure
23
+ ERROR = "error" # Operation failure
24
+ WARNING = "warning" # Recoverable issue
25
+ INFO = "info" # Informational
26
+ DEBUG = "debug" # Debug information
27
+
28
+
29
+ class ErrorCategory(Enum):
30
+ """Categories of errors for handling strategy"""
31
+ FILE_IO = "file_io"
32
+ PARSING = "parsing"
33
+ VALIDATION = "validation"
34
+ NETWORK = "network"
35
+ PERMISSION = "permission"
36
+ TYPE_CONVERSION = "type_conversion"
37
+ MISSING_DEPENDENCY = "missing_dependency"
38
+ CONFIGURATION = "configuration"
39
+ RUNTIME = "runtime"
40
+ UNKNOWN = "unknown"
41
+
42
+
43
+ @dataclass
44
+ class ErrorContext:
45
+ """Context information for error handling"""
46
+ error: Exception
47
+ category: ErrorCategory
48
+ severity: ErrorSeverity
49
+ source: Optional[str] = None
50
+ operation: Optional[str] = None
51
+ timestamp: datetime = field(default_factory=datetime.now)
52
+ traceback: Optional[str] = None
53
+ metadata: Dict[str, Any] = field(default_factory=dict)
54
+ recovery_attempted: bool = False
55
+ recovery_successful: bool = False
56
+
57
+
58
+ @dataclass
59
+ class ErrorHandlingResult:
60
+ """Result of error handling operation"""
61
+ handled: bool
62
+ recovered: bool = False
63
+ fallback_value: Any = None
64
+ should_retry: bool = False
65
+ retry_after: Optional[int] = None # seconds
66
+ should_escalate: bool = False
67
+ message: Optional[str] = None
68
+ actions_taken: List[str] = field(default_factory=list)
69
+
70
+
71
+ class BaseErrorHandler(ABC):
72
+ """Base class for all error handlers"""
73
+
74
+ def __init__(self):
75
+ self.logger = get_logger(self.__class__.__name__)
76
+
77
+ @abstractmethod
78
+ def can_handle(self, context: ErrorContext) -> bool:
79
+ """Check if this handler can handle the error"""
80
+ pass
81
+
82
+ @abstractmethod
83
+ def handle(self, context: ErrorContext) -> ErrorHandlingResult:
84
+ """Handle the error"""
85
+ pass
86
+
87
+ def log_error(self, context: ErrorContext, message: str = None):
88
+ """Log error with appropriate level"""
89
+ log_message = message or str(context.error)
90
+
91
+ if context.severity == ErrorSeverity.CRITICAL:
92
+ self.logger.critical(log_message)
93
+ elif context.severity == ErrorSeverity.ERROR:
94
+ self.logger.error(log_message)
95
+ elif context.severity == ErrorSeverity.WARNING:
96
+ self.logger.warning(log_message)
97
+ elif context.severity == ErrorSeverity.INFO:
98
+ self.logger.info(log_message)
99
+ else:
100
+ self.logger.debug(log_message)
101
+
102
+
103
+ class FileIOErrorHandler(BaseErrorHandler):
104
+ """Handles file I/O errors - consolidates 18 file error patterns"""
105
+
106
+ ERROR_MAPPING = {
107
+ FileNotFoundError: "File not found",
108
+ PermissionError: "Permission denied",
109
+ IsADirectoryError: "Path is a directory",
110
+ NotADirectoryError: "Path is not a directory",
111
+ IOError: "I/O operation failed",
112
+ OSError: "Operating system error"
113
+ }
114
+
115
+ def can_handle(self, context: ErrorContext) -> bool:
116
+ """Check if error is file I/O related"""
117
+ return (
118
+ context.category == ErrorCategory.FILE_IO or
119
+ isinstance(context.error, (FileNotFoundError, PermissionError, IOError, OSError))
120
+ )
121
+
122
+ def handle(self, context: ErrorContext) -> ErrorHandlingResult:
123
+ """Handle file I/O errors with recovery strategies"""
124
+ result = ErrorHandlingResult(handled=True)
125
+
126
+ error_type = type(context.error)
127
+ error_message = self.ERROR_MAPPING.get(error_type, "Unknown file error")
128
+
129
+ # Log the error
130
+ self.log_error(context, f"{error_message}: {context.source}")
131
+
132
+ # Try recovery strategies
133
+ if isinstance(context.error, FileNotFoundError):
134
+ result = self._handle_file_not_found(context)
135
+ elif isinstance(context.error, PermissionError):
136
+ result = self._handle_permission_error(context)
137
+ else:
138
+ result = self._handle_generic_io_error(context)
139
+
140
+ result.actions_taken.append(f"Handled {error_type.__name__}")
141
+ return result
142
+
143
+ def _handle_file_not_found(self, context: ErrorContext) -> ErrorHandlingResult:
144
+ """Handle file not found errors"""
145
+ result = ErrorHandlingResult(handled=True)
146
+
147
+ # Check for fallback locations
148
+ if context.metadata.get('fallback_paths'):
149
+ for fallback in context.metadata['fallback_paths']:
150
+ fallback_path = Path(fallback)
151
+ if fallback_path.exists():
152
+ result.recovered = True
153
+ result.fallback_value = str(fallback_path)
154
+ result.actions_taken.append(f"Used fallback path: {fallback_path}")
155
+ self.logger.info(f"Using fallback configuration: {fallback_path}")
156
+ return result
157
+
158
+ # Check for default values
159
+ if context.metadata.get('default_config'):
160
+ result.recovered = True
161
+ result.fallback_value = context.metadata['default_config']
162
+ result.actions_taken.append("Used default configuration")
163
+ return result
164
+
165
+ # Create file if requested
166
+ if context.metadata.get('create_if_missing'):
167
+ path = Path(context.source)
168
+ try:
169
+ path.parent.mkdir(parents=True, exist_ok=True)
170
+
171
+ # Create with default content
172
+ default_content = context.metadata.get('default_content', {})
173
+
174
+ if path.suffix == '.json':
175
+ path.write_text(json.dumps(default_content, indent=2))
176
+ else:
177
+ path.write_text(str(default_content))
178
+
179
+ result.recovered = True
180
+ result.should_retry = True
181
+ result.actions_taken.append(f"Created missing file: {path}")
182
+ self.logger.info(f"Created missing configuration file: {path}")
183
+
184
+ except Exception as e:
185
+ self.logger.error(f"Failed to create file: {e}")
186
+ result.should_escalate = True
187
+
188
+ return result
189
+
190
+ def _handle_permission_error(self, context: ErrorContext) -> ErrorHandlingResult:
191
+ """Handle permission errors"""
192
+ result = ErrorHandlingResult(handled=True)
193
+
194
+ # Try alternative location
195
+ if context.metadata.get('alt_location'):
196
+ alt_path = Path(context.metadata['alt_location'])
197
+ try:
198
+ # Test write permission
199
+ alt_path.parent.mkdir(parents=True, exist_ok=True)
200
+ test_file = alt_path.parent / '.test_write'
201
+ test_file.touch()
202
+ test_file.unlink()
203
+
204
+ result.recovered = True
205
+ result.fallback_value = str(alt_path)
206
+ result.actions_taken.append(f"Using alternative location: {alt_path}")
207
+
208
+ except:
209
+ result.should_escalate = True
210
+
211
+ # Use read-only mode if applicable
212
+ elif context.metadata.get('allow_readonly'):
213
+ result.recovered = True
214
+ result.fallback_value = {'readonly': True}
215
+ result.actions_taken.append("Switched to read-only mode")
216
+
217
+ return result
218
+
219
+ def _handle_generic_io_error(self, context: ErrorContext) -> ErrorHandlingResult:
220
+ """Handle generic I/O errors"""
221
+ result = ErrorHandlingResult(handled=True)
222
+
223
+ # Retry with exponential backoff
224
+ retry_count = context.metadata.get('retry_count', 0)
225
+ max_retries = context.metadata.get('max_retries', 3)
226
+
227
+ if retry_count < max_retries:
228
+ result.should_retry = True
229
+ result.retry_after = 2 ** retry_count # Exponential backoff
230
+ result.actions_taken.append(f"Retry {retry_count + 1}/{max_retries} after {result.retry_after}s")
231
+ else:
232
+ result.should_escalate = True
233
+ result.message = f"Failed after {max_retries} retries"
234
+
235
+ return result
236
+
237
+
238
+ class ParsingErrorHandler(BaseErrorHandler):
239
+ """Handles parsing errors - consolidates 22 parsing error patterns"""
240
+
241
+ PARSER_ERRORS = {
242
+ json.JSONDecodeError: ErrorCategory.PARSING,
243
+ ValueError: ErrorCategory.PARSING, # Common for parsing
244
+ SyntaxError: ErrorCategory.PARSING
245
+ }
246
+
247
+ def can_handle(self, context: ErrorContext) -> bool:
248
+ """Check if error is parsing related"""
249
+ return (
250
+ context.category == ErrorCategory.PARSING or
251
+ type(context.error) in self.PARSER_ERRORS or
252
+ 'parse' in str(context.error).lower() or
253
+ 'decode' in str(context.error).lower()
254
+ )
255
+
256
+ def handle(self, context: ErrorContext) -> ErrorHandlingResult:
257
+ """Handle parsing errors with recovery strategies"""
258
+ result = ErrorHandlingResult(handled=True)
259
+
260
+ # Try recovery strategies based on error type
261
+ if isinstance(context.error, json.JSONDecodeError):
262
+ result = self._handle_json_error(context)
263
+ elif 'yaml' in str(context.error).lower():
264
+ result = self._handle_yaml_error(context)
265
+ else:
266
+ result = self._handle_generic_parse_error(context)
267
+
268
+ return result
269
+
270
+ def _handle_json_error(self, context: ErrorContext) -> ErrorHandlingResult:
271
+ """Handle JSON parsing errors"""
272
+ result = ErrorHandlingResult(handled=True)
273
+
274
+ content = context.metadata.get('content', '')
275
+
276
+ # Try to fix common JSON issues
277
+ fixes = [
278
+ self._fix_json_comments,
279
+ self._fix_json_quotes,
280
+ self._fix_json_trailing_commas,
281
+ self._fix_json_unquoted_keys
282
+ ]
283
+
284
+ for fix_func in fixes:
285
+ try:
286
+ fixed_content = fix_func(content)
287
+ parsed = json.loads(fixed_content)
288
+ result.recovered = True
289
+ result.fallback_value = parsed
290
+ result.actions_taken.append(f"Fixed JSON with {fix_func.__name__}")
291
+ self.logger.info(f"Recovered from JSON error using {fix_func.__name__}")
292
+ return result
293
+ except:
294
+ continue
295
+
296
+ # Use lenient parser if available
297
+ if context.metadata.get('allow_lenient'):
298
+ result = self._parse_lenient_json(content, result)
299
+
300
+ return result
301
+
302
+ def _fix_json_comments(self, content: str) -> str:
303
+ """Remove comments from JSON"""
304
+ import re
305
+ # Remove single-line comments
306
+ content = re.sub(r'//.*?$', '', content, flags=re.MULTILINE)
307
+ # Remove multi-line comments
308
+ content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL)
309
+ return content
310
+
311
+ def _fix_json_quotes(self, content: str) -> str:
312
+ """Fix quote issues in JSON"""
313
+ import re
314
+ # Replace single quotes with double quotes (careful with values)
315
+ # This is a simple approach - more sophisticated parsing might be needed
316
+ content = re.sub(r"'([^']*)':", r'"\1":', content) # Keys
317
+ content = re.sub(r":\s*'([^']*)'", r': "\1"', content) # Values
318
+ return content
319
+
320
+ def _fix_json_trailing_commas(self, content: str) -> str:
321
+ """Remove trailing commas"""
322
+ import re
323
+ content = re.sub(r',\s*}', '}', content)
324
+ content = re.sub(r',\s*]', ']', content)
325
+ return content
326
+
327
+ def _fix_json_unquoted_keys(self, content: str) -> str:
328
+ """Add quotes to unquoted keys"""
329
+ import re
330
+ # Match unquoted keys (word characters followed by colon)
331
+ content = re.sub(r'(\w+):', r'"\1":', content)
332
+ return content
333
+
334
+ def _parse_lenient_json(self, content: str, result: ErrorHandlingResult) -> ErrorHandlingResult:
335
+ """Parse JSON leniently"""
336
+ try:
337
+ # Try using ast.literal_eval for Python literals
338
+ import ast
339
+ parsed = ast.literal_eval(content)
340
+ result.recovered = True
341
+ result.fallback_value = parsed
342
+ result.actions_taken.append("Parsed as Python literal")
343
+ except:
344
+ # Return empty dict as last resort
345
+ result.recovered = True
346
+ result.fallback_value = {}
347
+ result.actions_taken.append("Used empty configuration as fallback")
348
+
349
+ return result
350
+
351
+ def _handle_yaml_error(self, context: ErrorContext) -> ErrorHandlingResult:
352
+ """Handle YAML parsing errors"""
353
+ result = ErrorHandlingResult(handled=True)
354
+
355
+ content = context.metadata.get('content', '')
356
+
357
+ # Try to fix common YAML issues
358
+ try:
359
+ import yaml
360
+
361
+ # Try with safe loader
362
+ parsed = yaml.safe_load(content)
363
+ result.recovered = True
364
+ result.fallback_value = parsed
365
+ result.actions_taken.append("Parsed with safe YAML loader")
366
+
367
+ except:
368
+ # Try to fix tabs
369
+ content = content.replace('\t', ' ')
370
+ try:
371
+ parsed = yaml.safe_load(content)
372
+ result.recovered = True
373
+ result.fallback_value = parsed
374
+ result.actions_taken.append("Fixed YAML tabs")
375
+ except:
376
+ result.fallback_value = {}
377
+ result.actions_taken.append("Used empty configuration as fallback")
378
+
379
+ return result
380
+
381
+ def _handle_generic_parse_error(self, context: ErrorContext) -> ErrorHandlingResult:
382
+ """Handle generic parsing errors"""
383
+ result = ErrorHandlingResult(handled=True)
384
+
385
+ # Try alternative formats
386
+ content = context.metadata.get('content', '')
387
+
388
+ formats = [
389
+ ('json', json.loads),
390
+ ('yaml', self._try_yaml),
391
+ ('ini', self._try_ini),
392
+ ('properties', self._try_properties)
393
+ ]
394
+
395
+ for format_name, parser in formats:
396
+ try:
397
+ parsed = parser(content)
398
+ if parsed:
399
+ result.recovered = True
400
+ result.fallback_value = parsed
401
+ result.actions_taken.append(f"Parsed as {format_name}")
402
+ return result
403
+ except:
404
+ continue
405
+
406
+ # Use default/empty config
407
+ result.recovered = True
408
+ result.fallback_value = context.metadata.get('default_config', {})
409
+ result.actions_taken.append("Used default configuration")
410
+
411
+ return result
412
+
413
+ def _try_yaml(self, content: str) -> Dict:
414
+ """Try parsing as YAML"""
415
+ import yaml
416
+ return yaml.safe_load(content)
417
+
418
+ def _try_ini(self, content: str) -> Dict:
419
+ """Try parsing as INI"""
420
+ import configparser
421
+ parser = configparser.ConfigParser()
422
+ parser.read_string(content)
423
+ return {s: dict(parser.items(s)) for s in parser.sections()}
424
+
425
+ def _try_properties(self, content: str) -> Dict:
426
+ """Try parsing as properties file"""
427
+ result = {}
428
+ for line in content.splitlines():
429
+ line = line.strip()
430
+ if line and not line.startswith('#') and '=' in line:
431
+ key, value = line.split('=', 1)
432
+ result[key.strip()] = value.strip()
433
+ return result
434
+
435
+
436
+ class ValidationErrorHandler(BaseErrorHandler):
437
+ """Handles validation errors - consolidates 15 validation error patterns"""
438
+
439
+ def can_handle(self, context: ErrorContext) -> bool:
440
+ """Check if error is validation related"""
441
+ return (
442
+ context.category == ErrorCategory.VALIDATION or
443
+ 'validation' in str(context.error).lower() or
444
+ 'invalid' in str(context.error).lower() or
445
+ 'constraint' in str(context.error).lower()
446
+ )
447
+
448
+ def handle(self, context: ErrorContext) -> ErrorHandlingResult:
449
+ """Handle validation errors"""
450
+ result = ErrorHandlingResult(handled=True)
451
+
452
+ # Get validation details
453
+ field = context.metadata.get('field')
454
+ value = context.metadata.get('value')
455
+ schema = context.metadata.get('schema')
456
+
457
+ # Try to fix or provide default
458
+ if field and schema:
459
+ result = self._fix_validation_error(field, value, schema, result)
460
+ else:
461
+ result = self._handle_generic_validation(context, result)
462
+
463
+ return result
464
+
465
+ def _fix_validation_error(
466
+ self,
467
+ field: str,
468
+ value: Any,
469
+ schema: Dict,
470
+ result: ErrorHandlingResult
471
+ ) -> ErrorHandlingResult:
472
+ """Try to fix validation error"""
473
+ field_schema = schema.get('properties', {}).get(field, {})
474
+
475
+ # Try type coercion
476
+ if 'type' in field_schema:
477
+ expected_type = field_schema['type']
478
+ coerced = self._coerce_type(value, expected_type)
479
+
480
+ if coerced is not None:
481
+ result.recovered = True
482
+ result.fallback_value = {field: coerced}
483
+ result.actions_taken.append(f"Coerced {field} to {expected_type}")
484
+ return result
485
+
486
+ # Use default value if available
487
+ if 'default' in field_schema:
488
+ result.recovered = True
489
+ result.fallback_value = {field: field_schema['default']}
490
+ result.actions_taken.append(f"Used default value for {field}")
491
+ return result
492
+
493
+ # Use minimum/maximum for range errors
494
+ if 'minimum' in field_schema and isinstance(value, (int, float)):
495
+ if value < field_schema['minimum']:
496
+ result.recovered = True
497
+ result.fallback_value = {field: field_schema['minimum']}
498
+ result.actions_taken.append(f"Clamped {field} to minimum")
499
+ return result
500
+
501
+ if 'maximum' in field_schema and isinstance(value, (int, float)):
502
+ if value > field_schema['maximum']:
503
+ result.recovered = True
504
+ result.fallback_value = {field: field_schema['maximum']}
505
+ result.actions_taken.append(f"Clamped {field} to maximum")
506
+ return result
507
+
508
+ return result
509
+
510
+ def _coerce_type(self, value: Any, expected_type: str) -> Any:
511
+ """Attempt to coerce value to expected type"""
512
+ try:
513
+ if expected_type == 'string':
514
+ return str(value)
515
+ elif expected_type == 'integer':
516
+ return int(value)
517
+ elif expected_type == 'number':
518
+ return float(value)
519
+ elif expected_type == 'boolean':
520
+ if isinstance(value, str):
521
+ return value.lower() in ['true', 'yes', '1', 'on']
522
+ return bool(value)
523
+ elif expected_type == 'array':
524
+ if isinstance(value, str):
525
+ # Try comma-separated
526
+ return [v.strip() for v in value.split(',')]
527
+ return list(value)
528
+ elif expected_type == 'object':
529
+ if isinstance(value, str):
530
+ return json.loads(value)
531
+ return dict(value)
532
+ except:
533
+ return None
534
+
535
+ def _handle_generic_validation(
536
+ self,
537
+ context: ErrorContext,
538
+ result: ErrorHandlingResult
539
+ ) -> ErrorHandlingResult:
540
+ """Handle generic validation errors"""
541
+ # Use strict vs lenient mode
542
+ if context.metadata.get('strict', True):
543
+ result.should_escalate = True
544
+ result.message = "Validation failed in strict mode"
545
+ else:
546
+ # In lenient mode, use config as-is with warnings
547
+ result.recovered = True
548
+ result.fallback_value = context.metadata.get('config', {})
549
+ result.actions_taken.append("Accepted configuration in lenient mode")
550
+ self.logger.warning(f"Validation error ignored in lenient mode: {context.error}")
551
+
552
+ return result
553
+
554
+
555
+ class NetworkErrorHandler(BaseErrorHandler):
556
+ """Handles network-related errors - consolidates 12 network error patterns"""
557
+
558
+ NETWORK_ERRORS = [
559
+ ConnectionError,
560
+ TimeoutError,
561
+ ConnectionRefusedError,
562
+ ConnectionResetError,
563
+ BrokenPipeError
564
+ ]
565
+
566
+ def can_handle(self, context: ErrorContext) -> bool:
567
+ """Check if error is network related"""
568
+ return (
569
+ context.category == ErrorCategory.NETWORK or
570
+ any(isinstance(context.error, err) for err in self.NETWORK_ERRORS) or
571
+ 'connection' in str(context.error).lower() or
572
+ 'timeout' in str(context.error).lower()
573
+ )
574
+
575
+ def handle(self, context: ErrorContext) -> ErrorHandlingResult:
576
+ """Handle network errors with retry logic"""
577
+ result = ErrorHandlingResult(handled=True)
578
+
579
+ # Implement exponential backoff retry
580
+ retry_count = context.metadata.get('retry_count', 0)
581
+ max_retries = context.metadata.get('max_retries', 5)
582
+
583
+ if retry_count < max_retries:
584
+ # Calculate backoff time
585
+ backoff = min(300, 2 ** retry_count) # Max 5 minutes
586
+ result.should_retry = True
587
+ result.retry_after = backoff
588
+ result.actions_taken.append(f"Retry {retry_count + 1}/{max_retries} after {backoff}s")
589
+
590
+ # Add jitter to prevent thundering herd
591
+ import random
592
+ result.retry_after += random.uniform(0, backoff * 0.1)
593
+
594
+ else:
595
+ # Try offline/cached mode
596
+ if context.metadata.get('cache_available'):
597
+ result.recovered = True
598
+ result.fallback_value = context.metadata.get('cached_config')
599
+ result.actions_taken.append("Using cached configuration")
600
+ else:
601
+ result.should_escalate = True
602
+ result.message = f"Network error after {max_retries} retries"
603
+
604
+ return result
605
+
606
+
607
+ class TypeConversionErrorHandler(BaseErrorHandler):
608
+ """Handles type conversion errors - consolidates 10 type conversion patterns"""
609
+
610
+ def can_handle(self, context: ErrorContext) -> bool:
611
+ """Check if error is type conversion related"""
612
+ return (
613
+ context.category == ErrorCategory.TYPE_CONVERSION or
614
+ isinstance(context.error, (TypeError, ValueError)) or
615
+ 'type' in str(context.error).lower() or
616
+ 'convert' in str(context.error).lower()
617
+ )
618
+
619
+ def handle(self, context: ErrorContext) -> ErrorHandlingResult:
620
+ """Handle type conversion errors"""
621
+ result = ErrorHandlingResult(handled=True)
622
+
623
+ source_value = context.metadata.get('value')
624
+ target_type = context.metadata.get('target_type')
625
+
626
+ if source_value is not None and target_type:
627
+ # Try intelligent conversion
628
+ converted = self._smart_convert(source_value, target_type)
629
+
630
+ if converted is not None:
631
+ result.recovered = True
632
+ result.fallback_value = converted
633
+ result.actions_taken.append(f"Converted to {target_type}")
634
+ else:
635
+ # Use default for type
636
+ default = self._get_type_default(target_type)
637
+ result.recovered = True
638
+ result.fallback_value = default
639
+ result.actions_taken.append(f"Used default for {target_type}")
640
+
641
+ return result
642
+
643
+ def _smart_convert(self, value: Any, target_type: Type) -> Any:
644
+ """Smart type conversion with fallbacks"""
645
+ converters = {
646
+ str: self._to_string,
647
+ int: self._to_int,
648
+ float: self._to_float,
649
+ bool: self._to_bool,
650
+ list: self._to_list,
651
+ dict: self._to_dict
652
+ }
653
+
654
+ converter = converters.get(target_type)
655
+ if converter:
656
+ try:
657
+ return converter(value)
658
+ except:
659
+ pass
660
+
661
+ return None
662
+
663
+ def _to_string(self, value: Any) -> str:
664
+ """Convert to string"""
665
+ if isinstance(value, bytes):
666
+ return value.decode('utf-8', errors='replace')
667
+ return str(value)
668
+
669
+ def _to_int(self, value: Any) -> int:
670
+ """Convert to integer"""
671
+ if isinstance(value, str):
672
+ # Try to extract number from string
673
+ import re
674
+ match = re.search(r'-?\d+', value)
675
+ if match:
676
+ return int(match.group())
677
+ return int(float(value))
678
+
679
+ def _to_float(self, value: Any) -> float:
680
+ """Convert to float"""
681
+ if isinstance(value, str):
682
+ # Handle percentage
683
+ if '%' in value:
684
+ return float(value.replace('%', '')) / 100
685
+ # Handle comma as decimal separator
686
+ value = value.replace(',', '.')
687
+ return float(value)
688
+
689
+ def _to_bool(self, value: Any) -> bool:
690
+ """Convert to boolean"""
691
+ if isinstance(value, str):
692
+ return value.lower() in ['true', 'yes', '1', 'on', 'enabled']
693
+ return bool(value)
694
+
695
+ def _to_list(self, value: Any) -> list:
696
+ """Convert to list"""
697
+ if isinstance(value, str):
698
+ # Try JSON array
699
+ if value.startswith('['):
700
+ try:
701
+ return json.loads(value)
702
+ except:
703
+ pass
704
+ # Try comma-separated
705
+ return [v.strip() for v in value.split(',')]
706
+ elif hasattr(value, '__iter__') and not isinstance(value, (str, bytes, dict)):
707
+ return list(value)
708
+ return [value]
709
+
710
+ def _to_dict(self, value: Any) -> dict:
711
+ """Convert to dictionary"""
712
+ if isinstance(value, str):
713
+ # Try JSON object
714
+ try:
715
+ return json.loads(value)
716
+ except:
717
+ pass
718
+ # Try key=value pairs
719
+ result = {}
720
+ for pair in value.split(','):
721
+ if '=' in pair:
722
+ k, v = pair.split('=', 1)
723
+ result[k.strip()] = v.strip()
724
+ return result
725
+ elif hasattr(value, '__dict__'):
726
+ return vars(value)
727
+ return {}
728
+
729
+ def _get_type_default(self, target_type: Type) -> Any:
730
+ """Get default value for type"""
731
+ defaults = {
732
+ str: '',
733
+ int: 0,
734
+ float: 0.0,
735
+ bool: False,
736
+ list: [],
737
+ dict: {},
738
+ type(None): None
739
+ }
740
+ return defaults.get(target_type, None)
741
+
742
+
743
+ class CompositeErrorHandler(BaseErrorHandler):
744
+ """Orchestrates multiple error handlers - consolidates 22 composite patterns"""
745
+
746
+ def __init__(self):
747
+ super().__init__()
748
+ self.handlers = [
749
+ FileIOErrorHandler(),
750
+ ParsingErrorHandler(),
751
+ ValidationErrorHandler(),
752
+ NetworkErrorHandler(),
753
+ TypeConversionErrorHandler()
754
+ ]
755
+
756
+ def can_handle(self, context: ErrorContext) -> bool:
757
+ """Composite handler can handle any error"""
758
+ return True
759
+
760
+ def handle(self, context: ErrorContext) -> ErrorHandlingResult:
761
+ """Try multiple handlers in sequence"""
762
+ # First, try specific handlers
763
+ for handler in self.handlers:
764
+ if handler.can_handle(context):
765
+ result = handler.handle(context)
766
+
767
+ if result.recovered or not result.should_escalate:
768
+ return result
769
+
770
+ # If no specific handler worked, use fallback strategies
771
+ return self._handle_unknown_error(context)
772
+
773
+ def _handle_unknown_error(self, context: ErrorContext) -> ErrorHandlingResult:
774
+ """Handle unknown errors with generic strategies"""
775
+ result = ErrorHandlingResult(handled=True)
776
+
777
+ # Log the full error
778
+ self.logger.error(
779
+ f"Unknown error in {context.operation}: {context.error}",
780
+ exc_info=True
781
+ )
782
+
783
+ # Try generic recovery strategies
784
+ if context.metadata.get('default_config'):
785
+ result.recovered = True
786
+ result.fallback_value = context.metadata['default_config']
787
+ result.actions_taken.append("Used default configuration for unknown error")
788
+ elif context.metadata.get('skip_on_error'):
789
+ result.recovered = True
790
+ result.fallback_value = {}
791
+ result.actions_taken.append("Skipped configuration due to error")
792
+ else:
793
+ result.should_escalate = True
794
+ result.message = f"Unhandled error: {context.error}"
795
+
796
+ return result
797
+
798
+
799
+ class ErrorHandlingStrategy(IConfigStrategy):
800
+ """
801
+ Main error handling strategy
802
+ Unifies 99 error handling patterns into composable handlers
803
+ """
804
+
805
+ def __init__(self):
806
+ self.logger = get_logger(self.__class__.__name__)
807
+ self.composite_handler = CompositeErrorHandler()
808
+ self.error_history: List[ErrorContext] = []
809
+ self.recovery_strategies: Dict[str, Callable] = {}
810
+
811
+ def can_handle(self, source: Union[str, Path, Dict]) -> bool:
812
+ """Error handler can handle any source"""
813
+ return True
814
+
815
+ def load(self, source: Any, **kwargs) -> Dict[str, Any]:
816
+ """Not used for error handling"""
817
+ return {}
818
+
819
+ def validate(self, config: Dict[str, Any], schema: Optional[Dict] = None) -> bool:
820
+ """Validate with error handling"""
821
+ return True
822
+
823
+ def transform(self, config: Dict[str, Any]) -> Dict[str, Any]:
824
+ """Transform config with error handling"""
825
+ return config
826
+
827
+ def handle_error(
828
+ self,
829
+ error: Exception,
830
+ source: Optional[str] = None,
831
+ operation: Optional[str] = None,
832
+ **metadata
833
+ ) -> ErrorHandlingResult:
834
+ """Main error handling entry point"""
835
+ # Categorize error
836
+ category = self._categorize_error(error)
837
+ severity = self._determine_severity(error, category)
838
+
839
+ # Create error context
840
+ context = ErrorContext(
841
+ error=error,
842
+ category=category,
843
+ severity=severity,
844
+ source=source,
845
+ operation=operation,
846
+ traceback=traceback.format_exc(),
847
+ metadata=metadata
848
+ )
849
+
850
+ # Record in history
851
+ self.error_history.append(context)
852
+
853
+ # Handle the error
854
+ result = self.composite_handler.handle(context)
855
+
856
+ # Apply recovery strategies if needed
857
+ if not result.recovered and self.recovery_strategies:
858
+ result = self._apply_recovery_strategies(context, result)
859
+
860
+ # Update context
861
+ context.recovery_attempted = result.recovered or result.should_retry
862
+ context.recovery_successful = result.recovered
863
+
864
+ return result
865
+
866
+ def _categorize_error(self, error: Exception) -> ErrorCategory:
867
+ """Categorize the error type"""
868
+ error_type = type(error)
869
+
870
+ # File I/O errors
871
+ if isinstance(error, (FileNotFoundError, PermissionError, IOError, OSError)):
872
+ return ErrorCategory.FILE_IO
873
+
874
+ # Parsing errors
875
+ if isinstance(error, (json.JSONDecodeError, ValueError, SyntaxError)):
876
+ if 'parse' in str(error).lower() or 'decode' in str(error).lower():
877
+ return ErrorCategory.PARSING
878
+
879
+ # Network errors
880
+ if isinstance(error, (ConnectionError, TimeoutError)):
881
+ return ErrorCategory.NETWORK
882
+
883
+ # Type conversion errors
884
+ if isinstance(error, TypeError):
885
+ return ErrorCategory.TYPE_CONVERSION
886
+
887
+ # Check error message for hints
888
+ error_msg = str(error).lower()
889
+
890
+ if 'validation' in error_msg or 'invalid' in error_msg:
891
+ return ErrorCategory.VALIDATION
892
+ elif 'permission' in error_msg or 'access' in error_msg:
893
+ return ErrorCategory.PERMISSION
894
+ elif 'not found' in error_msg or 'missing' in error_msg:
895
+ return ErrorCategory.MISSING_DEPENDENCY
896
+ elif 'config' in error_msg or 'setting' in error_msg:
897
+ return ErrorCategory.CONFIGURATION
898
+
899
+ return ErrorCategory.UNKNOWN
900
+
901
+ def _determine_severity(self, error: Exception, category: ErrorCategory) -> ErrorSeverity:
902
+ """Determine error severity"""
903
+ # Critical errors
904
+ critical_types = [MemoryError, SystemError, KeyboardInterrupt]
905
+ if type(error) in critical_types:
906
+ return ErrorSeverity.CRITICAL
907
+
908
+ # Category-based severity
909
+ severity_map = {
910
+ ErrorCategory.FILE_IO: ErrorSeverity.ERROR,
911
+ ErrorCategory.PARSING: ErrorSeverity.WARNING,
912
+ ErrorCategory.VALIDATION: ErrorSeverity.WARNING,
913
+ ErrorCategory.NETWORK: ErrorSeverity.ERROR,
914
+ ErrorCategory.PERMISSION: ErrorSeverity.ERROR,
915
+ ErrorCategory.TYPE_CONVERSION: ErrorSeverity.WARNING,
916
+ ErrorCategory.MISSING_DEPENDENCY: ErrorSeverity.ERROR,
917
+ ErrorCategory.CONFIGURATION: ErrorSeverity.ERROR,
918
+ ErrorCategory.RUNTIME: ErrorSeverity.ERROR,
919
+ ErrorCategory.UNKNOWN: ErrorSeverity.ERROR
920
+ }
921
+
922
+ return severity_map.get(category, ErrorSeverity.ERROR)
923
+
924
+ def _apply_recovery_strategies(
925
+ self,
926
+ context: ErrorContext,
927
+ result: ErrorHandlingResult
928
+ ) -> ErrorHandlingResult:
929
+ """Apply custom recovery strategies"""
930
+ for name, strategy in self.recovery_strategies.items():
931
+ try:
932
+ recovery_result = strategy(context)
933
+ if recovery_result:
934
+ result.recovered = True
935
+ result.fallback_value = recovery_result
936
+ result.actions_taken.append(f"Applied recovery strategy: {name}")
937
+ return result
938
+ except Exception as e:
939
+ self.logger.debug(f"Recovery strategy {name} failed: {e}")
940
+
941
+ return result
942
+
943
+ def register_recovery_strategy(self, name: str, strategy: Callable):
944
+ """Register a custom recovery strategy"""
945
+ self.recovery_strategies[name] = strategy
946
+ self.logger.debug(f"Registered recovery strategy: {name}")
947
+
948
+ def get_error_statistics(self) -> Dict[str, Any]:
949
+ """Get error handling statistics"""
950
+ if not self.error_history:
951
+ return {
952
+ 'total_errors': 0,
953
+ 'categories': {},
954
+ 'severities': {},
955
+ 'recovery_rate': 0.0
956
+ }
957
+
958
+ total = len(self.error_history)
959
+ recovered = sum(1 for e in self.error_history if e.recovery_successful)
960
+
961
+ categories = {}
962
+ severities = {}
963
+
964
+ for error in self.error_history:
965
+ # Count by category
966
+ cat_name = error.category.value
967
+ categories[cat_name] = categories.get(cat_name, 0) + 1
968
+
969
+ # Count by severity
970
+ sev_name = error.severity.value
971
+ severities[sev_name] = severities.get(sev_name, 0) + 1
972
+
973
+ return {
974
+ 'total_errors': total,
975
+ 'recovered': recovered,
976
+ 'recovery_rate': (recovered / total) * 100 if total > 0 else 0,
977
+ 'categories': categories,
978
+ 'severities': severities,
979
+ 'recent_errors': [
980
+ {
981
+ 'timestamp': e.timestamp.isoformat(),
982
+ 'category': e.category.value,
983
+ 'severity': e.severity.value,
984
+ 'operation': e.operation,
985
+ 'recovered': e.recovery_successful
986
+ }
987
+ for e in self.error_history[-10:] # Last 10 errors
988
+ ]
989
+ }
990
+
991
+
992
+ # Export main components
993
+ __all__ = [
994
+ 'ErrorHandlingStrategy',
995
+ 'ErrorContext',
996
+ 'ErrorHandlingResult',
997
+ 'ErrorCategory',
998
+ 'ErrorSeverity'
999
+ ]