awesome-errors 0.3.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.
Files changed (37) hide show
  1. awesome_errors/__init__.py +232 -0
  2. awesome_errors/analysis/__init__.py +8 -0
  3. awesome_errors/analysis/decorators.py +251 -0
  4. awesome_errors/analysis/error_analyzer.py +463 -0
  5. awesome_errors/client/__init__.py +7 -0
  6. awesome_errors/client/exceptions.py +97 -0
  7. awesome_errors/client/response_parser.py +175 -0
  8. awesome_errors/converters/__init__.py +31 -0
  9. awesome_errors/converters/generic.py +23 -0
  10. awesome_errors/converters/pydantic_converter.py +75 -0
  11. awesome_errors/converters/python_converter.py +80 -0
  12. awesome_errors/converters/sql_converter.py +258 -0
  13. awesome_errors/converters/universal_converter.py +123 -0
  14. awesome_errors/core/__init__.py +0 -0
  15. awesome_errors/core/error_codes.py +104 -0
  16. awesome_errors/core/error_response.py +61 -0
  17. awesome_errors/core/exceptions.py +613 -0
  18. awesome_errors/core/renderers.py +115 -0
  19. awesome_errors/i18n/__init__.py +0 -0
  20. awesome_errors/i18n/locales/en/errors.json +32 -0
  21. awesome_errors/i18n/locales/test/errors.json +4 -0
  22. awesome_errors/i18n/locales/uk/errors.json +26 -0
  23. awesome_errors/i18n/translator.py +150 -0
  24. awesome_errors/integrations/__init__.py +11 -0
  25. awesome_errors/integrations/fastapi_auto_docs.py +220 -0
  26. awesome_errors/litestar_utils.py +55 -0
  27. awesome_errors/middleware/__init__.py +0 -0
  28. awesome_errors/middleware/fastapi.py +247 -0
  29. awesome_errors/middleware/litestar.py +333 -0
  30. awesome_errors/py.typed +1 -0
  31. awesome_errors/websocket/__init__.py +35 -0
  32. awesome_errors/websocket/error_handler.py +231 -0
  33. awesome_errors/websocket/exceptions.py +286 -0
  34. awesome_errors-0.3.0.dist-info/METADATA +239 -0
  35. awesome_errors-0.3.0.dist-info/RECORD +37 -0
  36. awesome_errors-0.3.0.dist-info/WHEEL +4 -0
  37. awesome_errors-0.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,175 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, Dict, Optional, Protocol, Union, cast
5
+
6
+ from .exceptions import BackendError
7
+ from ..core.error_response import ErrorDetail, error_detail_from_mapping
8
+
9
+
10
+ class _HasDetails(Protocol):
11
+ details: Dict[str, Any]
12
+
13
+
14
+ class ErrorDetailMixin:
15
+ @property
16
+ def field(self) -> Any:
17
+ return cast(_HasDetails, self).details.get("field")
18
+
19
+ @property
20
+ def field_errors(self) -> Any:
21
+ return cast(_HasDetails, self).details.get("field_errors")
22
+
23
+ @property
24
+ def table(self) -> Any:
25
+ return cast(_HasDetails, self).details.get("table")
26
+
27
+ @property
28
+ def constraint(self) -> Any:
29
+ return cast(_HasDetails, self).details.get("constraint")
30
+
31
+ @property
32
+ def duplicate_value(self) -> Any:
33
+ return cast(_HasDetails, self).details.get("duplicate_value")
34
+
35
+
36
+ class ErrorResponseParser:
37
+ """Parser for backend error responses."""
38
+
39
+ @classmethod
40
+ def parse_response(
41
+ cls,
42
+ response_body: Union[str, bytes, Dict[str, Any]],
43
+ status_code: int,
44
+ headers: Optional[Dict[str, str]] = None,
45
+ ) -> BackendError:
46
+ """
47
+ Parse error response from backend.
48
+
49
+ Args:
50
+ response_body: Response body (JSON string, bytes, or dict)
51
+ status_code: HTTP status code
52
+ headers: Response headers
53
+
54
+ Returns:
55
+ BackendError instance
56
+
57
+ Raises:
58
+ ValueError: If response cannot be parsed
59
+ """
60
+ try:
61
+ data = (
62
+ json.loads(response_body)
63
+ if isinstance(response_body, (str, bytes))
64
+ else response_body
65
+ )
66
+
67
+ if not isinstance(data, dict):
68
+ raise ValueError("Response body is not a JSON object")
69
+
70
+ detail_payload: Optional[Dict[str, Any]] = None
71
+ if cls.is_error_response(data):
72
+ error_block = data.get("error", {})
73
+ if isinstance(error_block, dict):
74
+ detail_payload = dict(error_block)
75
+ elif cls.is_problem_response(data):
76
+ detail_payload = cls._problem_detail_to_error_payload(data)
77
+
78
+ if detail_payload is None:
79
+ raise ValueError("Unsupported error response shape")
80
+
81
+ request_id = detail_payload.get("request_id")
82
+ if not request_id:
83
+ request_id = headers.get("X-Request-ID") if headers else None
84
+ detail_payload["request_id"] = request_id or "unknown"
85
+
86
+ error_detail = error_detail_from_mapping(detail_payload)
87
+
88
+ return BackendError(
89
+ error=error_detail,
90
+ status_code=status_code,
91
+ response_headers=headers,
92
+ )
93
+
94
+ except (json.JSONDecodeError, ValueError, TypeError) as e:
95
+ # Fallback for non-standard error responses
96
+ return cls._create_fallback_error(response_body, status_code, headers, e)
97
+
98
+ @classmethod
99
+ def _create_fallback_error(
100
+ cls,
101
+ response_body: Any,
102
+ status_code: int,
103
+ headers: Optional[Dict[str, str]],
104
+ parse_error: Exception,
105
+ ) -> BackendError:
106
+ """Create fallback error when parsing fails."""
107
+ # Try to extract message from response
108
+ message = "Unknown error"
109
+ details: Dict[str, Any] = {
110
+ "parse_error": str(parse_error),
111
+ "response_type": type(response_body).__name__,
112
+ }
113
+
114
+ if isinstance(response_body, dict):
115
+ # Try common error fields
116
+ message = (
117
+ response_body.get("message")
118
+ or response_body.get("error")
119
+ or response_body.get("detail")
120
+ or str(response_body)
121
+ )
122
+ details["raw_response"] = response_body
123
+ elif isinstance(response_body, (str, bytes)):
124
+ message = str(response_body)[:200] # Limit length
125
+ details["raw_response"] = message
126
+
127
+ # Create error detail
128
+ error_detail = ErrorDetail(
129
+ code="UNKNOWN_ERROR",
130
+ message=message,
131
+ details=details,
132
+ request_id=headers.get("X-Request-ID", "unknown") if headers else "unknown",
133
+ )
134
+
135
+ return BackendError(
136
+ error=error_detail, status_code=status_code, response_headers=headers
137
+ )
138
+
139
+ @classmethod
140
+ def is_error_response(cls, response_data: Dict[str, Any]) -> bool:
141
+ """
142
+ Check if response data is an error response.
143
+
144
+ Args:
145
+ response_data: Response data as dictionary
146
+
147
+ Returns:
148
+ True if this is an error response
149
+ """
150
+ return "error" in response_data and isinstance(response_data.get("error"), dict)
151
+
152
+ @classmethod
153
+ def is_problem_response(cls, response_data: Dict[str, Any]) -> bool:
154
+ """Check whether the payload follows RFC 7807 conventions."""
155
+
156
+ required = {"type", "title", "status", "code"}
157
+ return required.issubset(response_data.keys())
158
+
159
+ @staticmethod
160
+ def _problem_detail_to_error_payload(data: Dict[str, Any]) -> Dict[str, Any]:
161
+ payload = {
162
+ "code": str(data.get("code", "UNKNOWN_ERROR")),
163
+ "message": data.get("title") or data.get("detail") or "Unknown error",
164
+ "request_id": data.get("request_id", ""),
165
+ "timestamp": data.get("timestamp"),
166
+ "details": dict(data.get("details") or {}),
167
+ }
168
+
169
+ # Carry over common meta fields into details for debugging purposes
170
+ details = payload["details"]
171
+ if "detail" in data and "explanation" not in details:
172
+ details.setdefault("detail", data.get("detail"))
173
+ if "instance" in data:
174
+ details.setdefault("instance", data.get("instance"))
175
+ return payload
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .python_converter import PythonErrorConverter
6
+ from .sql_converter import SQLErrorConverter
7
+ from .universal_converter import UniversalErrorConverter
8
+
9
+ PydanticErrorConverter: Any
10
+ try: # pragma: no cover - optional dependency
11
+ from .pydantic_converter import PydanticErrorConverter as PydanticErrorConverter
12
+ except ImportError: # pragma: no cover
13
+
14
+ class _FallbackPydanticErrorConverter:
15
+ """Placeholder raising a helpful message when pydantic is missing."""
16
+
17
+ @staticmethod
18
+ def convert(*_args: Any, **_kwargs: Any) -> Any:
19
+ raise ImportError(
20
+ "Install 'awesome-errors[pydantic]' to enable Pydantic error conversions."
21
+ )
22
+
23
+ PydanticErrorConverter = _FallbackPydanticErrorConverter
24
+
25
+
26
+ __all__ = [
27
+ "SQLErrorConverter",
28
+ "PythonErrorConverter",
29
+ "PydanticErrorConverter",
30
+ "UniversalErrorConverter",
31
+ ]
@@ -0,0 +1,23 @@
1
+ from ..core.exceptions import AppError
2
+ from ..core.error_codes import ErrorCode
3
+
4
+
5
+ def generic_error_handler(error: Exception, debug: bool = False) -> AppError:
6
+ error_type = type(error).__name__
7
+ error_module = type(error).__module__
8
+ details: dict[str, object] = {
9
+ "error_type": error_type,
10
+ "error_module": error_module,
11
+ }
12
+ if debug:
13
+ details["error_str"] = str(error)
14
+ details["error_repr"] = repr(error)
15
+ if hasattr(error, "__dict__"):
16
+ details["error_attrs"] = {
17
+ k: str(v) for k, v in error.__dict__.items() if not k.startswith("_")
18
+ }
19
+ return AppError(
20
+ code=ErrorCode.INTERNAL_ERROR,
21
+ message=f"Unexpected error: {error_type}",
22
+ details=details,
23
+ )
@@ -0,0 +1,75 @@
1
+ from typing import Any, Dict, List, Mapping, Sequence
2
+ from pydantic import ValidationError as PydanticValidationError
3
+
4
+ from ..core.error_codes import ErrorCode
5
+ from ..core.exceptions import ValidationError
6
+
7
+
8
+ class PydanticErrorConverter:
9
+ """Convert Pydantic validation errors to application errors."""
10
+
11
+ @classmethod
12
+ def convert(cls, error: PydanticValidationError) -> ValidationError:
13
+ """
14
+ Convert Pydantic ValidationError to application ValidationError.
15
+
16
+ Args:
17
+ error: Pydantic validation error
18
+
19
+ Returns:
20
+ Application ValidationError with detailed field information
21
+ """
22
+ errors = error.errors()
23
+
24
+ # Extract first error for main message
25
+ first_error: Mapping[str, Any] | None = errors[0] if errors else None
26
+ main_field = cls._get_field_path(first_error.get("loc", [])) if first_error else ""
27
+ main_message = first_error.get("msg", "Validation failed") if first_error else "Validation failed"
28
+
29
+ # Build detailed error information
30
+ field_errors = cls._build_field_errors(errors)
31
+
32
+ # Create validation error with detailed field information
33
+ validation_error = ValidationError(
34
+ message=f"Validation failed for field '{main_field}': {main_message}"
35
+ if main_field
36
+ else main_message,
37
+ code=ErrorCode.VALIDATION_FAILED,
38
+ )
39
+
40
+ # Add detailed field errors
41
+ validation_error.details = {
42
+ "field_errors": field_errors,
43
+ "error_count": len(errors),
44
+ }
45
+
46
+ return validation_error
47
+
48
+ @classmethod
49
+ def _build_field_errors(
50
+ cls, errors: Sequence[Mapping[str, Any]]
51
+ ) -> List[Dict[str, Any]]:
52
+ """Build detailed field error information."""
53
+ field_errors = []
54
+
55
+ for error in errors:
56
+ field_path = cls._get_field_path(error.get("loc", []))
57
+ field_error = {
58
+ "field": field_path,
59
+ "message": error.get("msg", "Invalid value"),
60
+ "type": error.get("type", "unknown"),
61
+ "context": error.get("ctx", {}),
62
+ }
63
+
64
+ # Add input value if available
65
+ if "input" in error:
66
+ field_error["input"] = error["input"]
67
+
68
+ field_errors.append(field_error)
69
+
70
+ return field_errors
71
+
72
+ @classmethod
73
+ def _get_field_path(cls, loc: Sequence[Any]) -> str:
74
+ """Convert location list to field path string."""
75
+ return ".".join(str(part) for part in loc) if loc else ""
@@ -0,0 +1,80 @@
1
+ from typing import Dict, Type, Tuple
2
+
3
+ from ..core.error_codes import ErrorCode
4
+ from ..core.exceptions import AppError, ValidationError, AuthError, NotFoundError
5
+ from .generic import generic_error_handler
6
+
7
+
8
+ class PythonErrorConverter:
9
+ """Convert standard Python exceptions to application errors."""
10
+
11
+ # Mapping of Python exceptions to (ErrorCode, message) tuples
12
+ EXCEPTION_MAP: Dict[Type[Exception], Tuple[ErrorCode, str]] = {
13
+ # Value errors
14
+ ValueError: (ErrorCode.INVALID_INPUT, "Invalid value provided"),
15
+ TypeError: (ErrorCode.INVALID_INPUT, "Invalid type provided"),
16
+ AttributeError: (ErrorCode.INVALID_INPUT, "Invalid attribute access"),
17
+ # Key/Index errors
18
+ KeyError: (ErrorCode.RESOURCE_NOT_FOUND, "Key not found"),
19
+ IndexError: (ErrorCode.RESOURCE_NOT_FOUND, "Index out of range"),
20
+ # Permission errors
21
+ PermissionError: (ErrorCode.AUTH_PERMISSION_DENIED, "Permission denied"),
22
+ # File errors
23
+ FileNotFoundError: (ErrorCode.RESOURCE_NOT_FOUND, "File not found"),
24
+ # Arithmetic errors
25
+ ZeroDivisionError: (ErrorCode.INVALID_INPUT, "Division by zero"),
26
+ ArithmeticError: (ErrorCode.INVALID_INPUT, "Arithmetic error"),
27
+ # System errors
28
+ MemoryError: (ErrorCode.INTERNAL_ERROR, "Memory error"),
29
+ RecursionError: (ErrorCode.INTERNAL_ERROR, "Maximum recursion depth exceeded"),
30
+ # Connection errors
31
+ ConnectionError: (ErrorCode.DB_CONNECTION_ERROR, "Connection error"),
32
+ TimeoutError: (ErrorCode.DB_CONNECTION_ERROR, "Operation timed out"),
33
+ }
34
+
35
+ @classmethod
36
+ def convert(cls, error: Exception) -> AppError:
37
+ """
38
+ Convert Python exception to AppError.
39
+
40
+ Args:
41
+ error: Python exception
42
+
43
+ Returns:
44
+ AppError instance
45
+ """
46
+ error_type = type(error)
47
+
48
+ # Check if we have a specific mapping
49
+ for exc_type, (code, default_message) in cls.EXCEPTION_MAP.items():
50
+ if issubclass(error_type, exc_type):
51
+ return cls._create_app_error(error, code, default_message)
52
+
53
+ # Default to internal error
54
+ return generic_error_handler(error)
55
+
56
+ @classmethod
57
+ def _create_app_error(
58
+ cls, error: Exception, code: ErrorCode, default_message: str
59
+ ) -> AppError:
60
+ """Create appropriate AppError subclass."""
61
+ message = str(error) or default_message
62
+ details = {"exception_type": type(error).__name__}
63
+
64
+ # Add specific details based on exception type
65
+ if isinstance(error, KeyError):
66
+ details["key"] = str(error).strip("'\"")
67
+ return NotFoundError(resource="key", resource_id=details["key"], code=code)
68
+
69
+ elif isinstance(error, FileNotFoundError):
70
+ details["filename"] = error.filename
71
+ return NotFoundError(resource="file", resource_id=error.filename, code=code)
72
+
73
+ elif isinstance(error, (ValueError, TypeError, AttributeError)):
74
+ return ValidationError(message=message, code=code)
75
+
76
+ elif isinstance(error, PermissionError):
77
+ return AuthError(message=message, code=code)
78
+
79
+ # Default to AppError
80
+ return AppError(code=code, message=message, details=details)
@@ -0,0 +1,258 @@
1
+ import re
2
+ from typing import Any, Dict, Optional, Tuple
3
+
4
+ from ..core.error_codes import ErrorCode
5
+ from ..core.exceptions import DatabaseError
6
+
7
+
8
+ class SQLErrorConverter:
9
+ """Convert SQLAlchemy errors to application errors."""
10
+
11
+ # Regex patterns for common SQL errors
12
+ SQL_PATTERNS: Dict[re.Pattern, Tuple[ErrorCode, str]] = {
13
+ # PostgreSQL patterns
14
+ re.compile(r"duplicate key value violates unique constraint"): (
15
+ ErrorCode.DB_DUPLICATE_ENTRY,
16
+ "Record already exists",
17
+ ),
18
+ re.compile(r"violates foreign key constraint"): (
19
+ ErrorCode.DB_INVALID_REFERENCE,
20
+ "Invalid reference to related record",
21
+ ),
22
+ re.compile(r"violates not-null constraint"): (
23
+ ErrorCode.DB_MISSING_REQUIRED,
24
+ "Required field is missing",
25
+ ),
26
+ re.compile(r"violates check constraint"): (
27
+ ErrorCode.DB_CONSTRAINT_VIOLATION,
28
+ "Value violates constraint",
29
+ ),
30
+ # MySQL patterns
31
+ re.compile(r"Duplicate entry .* for key"): (
32
+ ErrorCode.DB_DUPLICATE_ENTRY,
33
+ "Record already exists",
34
+ ),
35
+ re.compile(
36
+ r"Cannot add or update a child row: a foreign key constraint fails"
37
+ ): (ErrorCode.DB_INVALID_REFERENCE, "Invalid reference to related record"),
38
+ re.compile(r"Column .* cannot be null"): (
39
+ ErrorCode.DB_MISSING_REQUIRED,
40
+ "Required field is missing",
41
+ ),
42
+ # SQLite patterns
43
+ re.compile(r"UNIQUE constraint failed"): (
44
+ ErrorCode.DB_DUPLICATE_ENTRY,
45
+ "Record already exists",
46
+ ),
47
+ re.compile(r"FOREIGN KEY constraint failed"): (
48
+ ErrorCode.DB_INVALID_REFERENCE,
49
+ "Invalid reference to related record",
50
+ ),
51
+ re.compile(r"NOT NULL constraint failed"): (
52
+ ErrorCode.DB_MISSING_REQUIRED,
53
+ "Required field is missing",
54
+ ),
55
+ }
56
+
57
+ @classmethod
58
+ def convert(cls, error: Exception) -> DatabaseError:
59
+ """Convert SQLAlchemy error to DatabaseError.
60
+
61
+ SQLAlchemy is imported lazily so the rest of the library can be used
62
+ without SQLAlchemy installed (install the ``sqlalchemy`` extra to use
63
+ this converter).
64
+ """
65
+ from sqlalchemy.exc import (
66
+ IntegrityError,
67
+ DataError,
68
+ OperationalError,
69
+ ProgrammingError,
70
+ DatabaseError as SQLAlchemyDatabaseError,
71
+ )
72
+
73
+ if isinstance(error, IntegrityError):
74
+ return cls._convert_integrity_error(error)
75
+ elif isinstance(error, DataError):
76
+ return cls._convert_data_error(error)
77
+ elif isinstance(error, OperationalError):
78
+ return cls._convert_operational_error(error)
79
+ elif isinstance(error, ProgrammingError):
80
+ return cls._convert_programming_error(error)
81
+ elif isinstance(error, SQLAlchemyDatabaseError):
82
+ return cls._convert_generic_database_error(error)
83
+ else:
84
+ return DatabaseError(
85
+ message=str(error), code=ErrorCode.DB_QUERY_ERROR, sql_error=str(error)
86
+ )
87
+
88
+ @classmethod
89
+ def _convert_integrity_error(cls, error: Any) -> DatabaseError:
90
+ """Convert IntegrityError to DatabaseError with detailed field information."""
91
+ error_str = str(error.orig) if error.orig else str(error)
92
+
93
+ # Check patterns
94
+ for pattern, (code, message) in cls.SQL_PATTERNS.items():
95
+ if pattern.search(error_str):
96
+ db_error = DatabaseError(
97
+ message=message,
98
+ code=code,
99
+ sql_error=error_str,
100
+ table=cls._extract_table_name(error_str),
101
+ )
102
+
103
+ # Add detailed field information
104
+ field_name = cls._extract_field_name(error_str)
105
+ if field_name:
106
+ db_error.details["field"] = field_name
107
+
108
+ constraint_name = cls._extract_constraint_name(error_str)
109
+ if constraint_name:
110
+ db_error.details["constraint"] = constraint_name
111
+
112
+ # For duplicate entries, extract the duplicate value
113
+ if code == ErrorCode.DB_DUPLICATE_ENTRY:
114
+ duplicate_value = cls._extract_duplicate_value(error_str)
115
+ if duplicate_value:
116
+ db_error.details["duplicate_value"] = duplicate_value
117
+ db_error.message = (
118
+ f"{message}: {field_name}={duplicate_value}"
119
+ if field_name
120
+ else f"{message}: {duplicate_value}"
121
+ )
122
+
123
+ # For missing required fields
124
+ elif code == ErrorCode.DB_MISSING_REQUIRED and field_name:
125
+ db_error.message = f"{message}: {field_name}"
126
+
127
+ # For invalid references
128
+ elif code == ErrorCode.DB_INVALID_REFERENCE and field_name:
129
+ db_error.message = f"{message} in field: {field_name}"
130
+
131
+ if db_error.details.get("table"):
132
+ db_error.details["table"] = db_error.details["table"]
133
+
134
+ return db_error
135
+
136
+ # Default integrity error
137
+ return DatabaseError(
138
+ message="Database integrity constraint violated",
139
+ code=ErrorCode.DB_CONSTRAINT_VIOLATION,
140
+ sql_error=error_str,
141
+ )
142
+
143
+ @classmethod
144
+ def _convert_data_error(cls, error: Any) -> DatabaseError:
145
+ """Convert DataError to DatabaseError."""
146
+ return DatabaseError(
147
+ message="Invalid data format",
148
+ code=ErrorCode.INVALID_FORMAT,
149
+ sql_error=str(error.orig) if error.orig else str(error),
150
+ )
151
+
152
+ @classmethod
153
+ def _convert_operational_error(cls, error: Any) -> DatabaseError:
154
+ """Convert OperationalError to DatabaseError."""
155
+ error_str = str(error.orig) if error.orig else str(error)
156
+
157
+ if "connection" in error_str.lower():
158
+ return DatabaseError(
159
+ message="Database connection error",
160
+ code=ErrorCode.DB_CONNECTION_ERROR,
161
+ sql_error=error_str,
162
+ )
163
+
164
+ return DatabaseError(
165
+ message="Database operational error",
166
+ code=ErrorCode.DB_QUERY_ERROR,
167
+ sql_error=error_str,
168
+ )
169
+
170
+ @classmethod
171
+ def _convert_programming_error(cls, error: Any) -> DatabaseError:
172
+ """Convert ProgrammingError to DatabaseError."""
173
+ return DatabaseError(
174
+ message="Database programming error",
175
+ code=ErrorCode.DB_QUERY_ERROR,
176
+ sql_error=str(error.orig) if error.orig else str(error),
177
+ )
178
+
179
+ @classmethod
180
+ def _convert_generic_database_error(
181
+ cls, error: Any
182
+ ) -> DatabaseError:
183
+ """Convert generic DatabaseError to DatabaseError."""
184
+ return DatabaseError(
185
+ message="Database error occurred",
186
+ code=ErrorCode.DB_QUERY_ERROR,
187
+ sql_error=str(error.orig) if error.orig else str(error),
188
+ )
189
+
190
+ @classmethod
191
+ def _extract_table_name(cls, error_str: str) -> Optional[str]:
192
+ """Extract table name from error string."""
193
+ # PostgreSQL pattern
194
+ match = re.search(r'relation "([^"]+)"', error_str)
195
+ if match:
196
+ return match.group(1)
197
+
198
+ # MySQL pattern
199
+ match = re.search(r"`([^`]+)`\.`([^`]+)`", error_str)
200
+ if match:
201
+ return match.group(2)
202
+
203
+ # SQLite pattern
204
+ match = re.search(r"table (\w+)", error_str)
205
+ if match:
206
+ return match.group(1)
207
+
208
+ return None
209
+
210
+ @classmethod
211
+ def _extract_field_name(cls, error_str: str) -> Optional[str]:
212
+ """Extract field/column name from error string."""
213
+ # PostgreSQL patterns
214
+ match = re.search(r'column "([^"]+)"', error_str)
215
+ if match:
216
+ return match.group(1)
217
+
218
+ # MySQL patterns
219
+ match = re.search(r"Column '([^']+)'", error_str)
220
+ if match:
221
+ return match.group(1)
222
+
223
+ # SQLite patterns
224
+ match = re.search(r"column (\w+)", error_str)
225
+ if match:
226
+ return match.group(1)
227
+
228
+ return None
229
+
230
+ @classmethod
231
+ def _extract_constraint_name(cls, error_str: str) -> Optional[str]:
232
+ """Extract constraint name from error string."""
233
+ # PostgreSQL pattern
234
+ match = re.search(r'constraint "([^"]+)"', error_str)
235
+ if match:
236
+ return match.group(1)
237
+
238
+ # MySQL pattern
239
+ match = re.search(r"key '([^']+)'", error_str)
240
+ if match:
241
+ return match.group(1)
242
+
243
+ return None
244
+
245
+ @classmethod
246
+ def _extract_duplicate_value(cls, error_str: str) -> Optional[str]:
247
+ """Extract duplicate value from error string."""
248
+ # PostgreSQL pattern
249
+ match = re.search(r"Key \([^)]+\)=\(([^)]+)\)", error_str)
250
+ if match:
251
+ return match.group(1)
252
+
253
+ # MySQL pattern
254
+ match = re.search(r"Duplicate entry '([^']+)'", error_str)
255
+ if match:
256
+ return match.group(1)
257
+
258
+ return None