claude-mpm 4.3.22__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.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/WORKFLOW.md +2 -14
- claude_mpm/cli/commands/configure.py +2 -29
- claude_mpm/cli/commands/doctor.py +2 -2
- claude_mpm/cli/commands/mpm_init.py +3 -3
- claude_mpm/cli/parsers/configure_parser.py +4 -15
- claude_mpm/core/framework/__init__.py +38 -0
- claude_mpm/core/framework/formatters/__init__.py +11 -0
- claude_mpm/core/framework/formatters/capability_generator.py +356 -0
- claude_mpm/core/framework/formatters/content_formatter.py +283 -0
- claude_mpm/core/framework/formatters/context_generator.py +180 -0
- claude_mpm/core/framework/loaders/__init__.py +13 -0
- claude_mpm/core/framework/loaders/agent_loader.py +202 -0
- claude_mpm/core/framework/loaders/file_loader.py +213 -0
- claude_mpm/core/framework/loaders/instruction_loader.py +151 -0
- claude_mpm/core/framework/loaders/packaged_loader.py +208 -0
- claude_mpm/core/framework/processors/__init__.py +11 -0
- claude_mpm/core/framework/processors/memory_processor.py +222 -0
- claude_mpm/core/framework/processors/metadata_processor.py +146 -0
- claude_mpm/core/framework/processors/template_processor.py +238 -0
- claude_mpm/core/framework_loader.py +277 -1798
- claude_mpm/hooks/__init__.py +9 -1
- claude_mpm/hooks/kuzu_memory_hook.py +352 -0
- claude_mpm/hooks/memory_integration_hook.py +1 -1
- claude_mpm/services/agents/memory/content_manager.py +5 -2
- claude_mpm/services/agents/memory/memory_file_service.py +1 -0
- claude_mpm/services/agents/memory/memory_limits_service.py +1 -0
- claude_mpm/services/core/path_resolver.py +1 -0
- claude_mpm/services/diagnostics/diagnostic_runner.py +1 -0
- claude_mpm/services/mcp_config_manager.py +67 -4
- claude_mpm/services/mcp_gateway/core/process_pool.py +281 -0
- claude_mpm/services/mcp_gateway/core/startup_verification.py +2 -2
- claude_mpm/services/mcp_gateway/main.py +3 -13
- claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -10
- claude_mpm/services/mcp_gateway/tools/__init__.py +13 -2
- claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +36 -6
- claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +542 -0
- claude_mpm/services/shared/__init__.py +2 -1
- claude_mpm/services/shared/service_factory.py +8 -5
- claude_mpm/services/unified/__init__.py +65 -0
- claude_mpm/services/unified/analyzer_strategies/__init__.py +44 -0
- claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +473 -0
- claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +643 -0
- claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +804 -0
- claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +661 -0
- claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +696 -0
- claude_mpm/services/unified/config_strategies/__init__.py +190 -0
- claude_mpm/services/unified/config_strategies/config_schema.py +689 -0
- claude_mpm/services/unified/config_strategies/context_strategy.py +748 -0
- claude_mpm/services/unified/config_strategies/error_handling_strategy.py +999 -0
- claude_mpm/services/unified/config_strategies/file_loader_strategy.py +871 -0
- claude_mpm/services/unified/config_strategies/unified_config_service.py +802 -0
- claude_mpm/services/unified/config_strategies/validation_strategy.py +1105 -0
- claude_mpm/services/unified/deployment_strategies/__init__.py +97 -0
- claude_mpm/services/unified/deployment_strategies/base.py +557 -0
- claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +486 -0
- claude_mpm/services/unified/deployment_strategies/local.py +594 -0
- claude_mpm/services/unified/deployment_strategies/utils.py +672 -0
- claude_mpm/services/unified/deployment_strategies/vercel.py +471 -0
- claude_mpm/services/unified/interfaces.py +499 -0
- claude_mpm/services/unified/migration.py +532 -0
- claude_mpm/services/unified/strategies.py +551 -0
- claude_mpm/services/unified/unified_analyzer.py +534 -0
- claude_mpm/services/unified/unified_config.py +688 -0
- claude_mpm/services/unified/unified_deployment.py +470 -0
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/METADATA +15 -15
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/RECORD +71 -32
- claude_mpm/cli/commands/configure_tui.py +0 -1927
- claude_mpm/services/mcp_gateway/tools/ticket_tools.py +0 -645
- claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +0 -602
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/WHEEL +0 -0
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.3.22.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
|
+
]
|