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.
- awesome_errors/__init__.py +232 -0
- awesome_errors/analysis/__init__.py +8 -0
- awesome_errors/analysis/decorators.py +251 -0
- awesome_errors/analysis/error_analyzer.py +463 -0
- awesome_errors/client/__init__.py +7 -0
- awesome_errors/client/exceptions.py +97 -0
- awesome_errors/client/response_parser.py +175 -0
- awesome_errors/converters/__init__.py +31 -0
- awesome_errors/converters/generic.py +23 -0
- awesome_errors/converters/pydantic_converter.py +75 -0
- awesome_errors/converters/python_converter.py +80 -0
- awesome_errors/converters/sql_converter.py +258 -0
- awesome_errors/converters/universal_converter.py +123 -0
- awesome_errors/core/__init__.py +0 -0
- awesome_errors/core/error_codes.py +104 -0
- awesome_errors/core/error_response.py +61 -0
- awesome_errors/core/exceptions.py +613 -0
- awesome_errors/core/renderers.py +115 -0
- awesome_errors/i18n/__init__.py +0 -0
- awesome_errors/i18n/locales/en/errors.json +32 -0
- awesome_errors/i18n/locales/test/errors.json +4 -0
- awesome_errors/i18n/locales/uk/errors.json +26 -0
- awesome_errors/i18n/translator.py +150 -0
- awesome_errors/integrations/__init__.py +11 -0
- awesome_errors/integrations/fastapi_auto_docs.py +220 -0
- awesome_errors/litestar_utils.py +55 -0
- awesome_errors/middleware/__init__.py +0 -0
- awesome_errors/middleware/fastapi.py +247 -0
- awesome_errors/middleware/litestar.py +333 -0
- awesome_errors/py.typed +1 -0
- awesome_errors/websocket/__init__.py +35 -0
- awesome_errors/websocket/error_handler.py +231 -0
- awesome_errors/websocket/exceptions.py +286 -0
- awesome_errors-0.3.0.dist-info/METADATA +239 -0
- awesome_errors-0.3.0.dist-info/RECORD +37 -0
- awesome_errors-0.3.0.dist-info/WHEEL +4 -0
- awesome_errors-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import inspect
|
|
3
|
+
from typing import Set, List, Dict, Any, Optional, Callable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ErrorAnalyzer(ast.NodeVisitor):
|
|
7
|
+
"""AST analyzer to find all possible errors in a function."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self, function: Callable, max_depth: int = 10, analyze_decorators: bool = True
|
|
11
|
+
):
|
|
12
|
+
"""
|
|
13
|
+
Initialize error analyzer.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
function: Function to analyze
|
|
17
|
+
max_depth: Maximum depth for recursive analysis
|
|
18
|
+
analyze_decorators: Whether to analyze decorators
|
|
19
|
+
"""
|
|
20
|
+
self.function = function
|
|
21
|
+
self.max_depth = max_depth
|
|
22
|
+
self.analyze_decorators = analyze_decorators
|
|
23
|
+
self.current_depth = 0
|
|
24
|
+
self.errors: Set[str] = set()
|
|
25
|
+
self.error_details: List[Dict[str, Any]] = []
|
|
26
|
+
self.visited_functions: Set[str] = set()
|
|
27
|
+
self.decorator_errors: List[Dict[str, Any]] = []
|
|
28
|
+
|
|
29
|
+
def analyze(self) -> Dict[str, Any]:
|
|
30
|
+
"""
|
|
31
|
+
Analyze function for all possible errors.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Dictionary with error analysis results
|
|
35
|
+
"""
|
|
36
|
+
# Reset state
|
|
37
|
+
self.errors.clear()
|
|
38
|
+
self.error_details.clear()
|
|
39
|
+
self.visited_functions.clear()
|
|
40
|
+
self.decorator_errors.clear()
|
|
41
|
+
self.current_depth = 0
|
|
42
|
+
|
|
43
|
+
# Analyze decorators first
|
|
44
|
+
if self.analyze_decorators:
|
|
45
|
+
self._analyze_decorators(self.function)
|
|
46
|
+
|
|
47
|
+
# Analyze the main function
|
|
48
|
+
self._analyze_function(self.function, is_main=True)
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
"function_name": self.function.__name__,
|
|
52
|
+
"error_codes": sorted(list(self.errors)),
|
|
53
|
+
"error_details": self.error_details,
|
|
54
|
+
"decorator_errors": self.decorator_errors,
|
|
55
|
+
"total_errors": len(self.errors),
|
|
56
|
+
"analysis_depth": self.current_depth,
|
|
57
|
+
"max_depth_reached": self.current_depth >= self.max_depth,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def _analyze_function(self, func: Callable, is_main: bool = False) -> None:
|
|
61
|
+
"""Analyze a specific function for errors with depth control."""
|
|
62
|
+
if self.current_depth >= self.max_depth and not is_main:
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
func_name = f"{func.__module__}.{func.__qualname__}"
|
|
66
|
+
|
|
67
|
+
# Avoid infinite recursion
|
|
68
|
+
if func_name in self.visited_functions:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
self.visited_functions.add(func_name)
|
|
72
|
+
|
|
73
|
+
if not is_main:
|
|
74
|
+
self.current_depth += 1
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# Get function source and parse AST
|
|
78
|
+
source = inspect.getsource(func)
|
|
79
|
+
# Remove leading indentation to avoid IndentationError
|
|
80
|
+
import textwrap
|
|
81
|
+
|
|
82
|
+
source = textwrap.dedent(source)
|
|
83
|
+
tree = ast.parse(source)
|
|
84
|
+
|
|
85
|
+
# Visit AST nodes
|
|
86
|
+
self.visit(tree)
|
|
87
|
+
|
|
88
|
+
except (OSError, TypeError):
|
|
89
|
+
# Can't get source - try to infer common library errors
|
|
90
|
+
self._analyze_builtin_function(func)
|
|
91
|
+
|
|
92
|
+
if not is_main:
|
|
93
|
+
self.current_depth -= 1
|
|
94
|
+
|
|
95
|
+
def visit_Raise(self, node: ast.Raise) -> None:
|
|
96
|
+
"""Handle raise statements."""
|
|
97
|
+
if node.exc:
|
|
98
|
+
error_info = self._extract_error_info(node.exc)
|
|
99
|
+
if error_info:
|
|
100
|
+
self.errors.add(error_info["code"])
|
|
101
|
+
self.error_details.append(error_info)
|
|
102
|
+
|
|
103
|
+
self.generic_visit(node)
|
|
104
|
+
|
|
105
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
106
|
+
"""Enhanced call analysis with context-aware error detection."""
|
|
107
|
+
# Analyze call context for known error patterns
|
|
108
|
+
self._analyze_call_context(node)
|
|
109
|
+
|
|
110
|
+
# Handle method calls (obj.method())
|
|
111
|
+
if isinstance(node.func, ast.Attribute):
|
|
112
|
+
self._analyze_method_call(node)
|
|
113
|
+
|
|
114
|
+
# Handle regular function calls
|
|
115
|
+
else:
|
|
116
|
+
called_func = self._resolve_function_call(node)
|
|
117
|
+
if called_func and self.current_depth < self.max_depth:
|
|
118
|
+
self._analyze_function(called_func)
|
|
119
|
+
|
|
120
|
+
self.generic_visit(node)
|
|
121
|
+
|
|
122
|
+
def _analyze_call_context(self, node: ast.Call) -> None:
|
|
123
|
+
"""Analyze call context to detect common error patterns."""
|
|
124
|
+
call_str = self._get_call_string(node)
|
|
125
|
+
|
|
126
|
+
# SQLAlchemy session operations
|
|
127
|
+
if any(
|
|
128
|
+
pattern in call_str
|
|
129
|
+
for pattern in [
|
|
130
|
+
"session.",
|
|
131
|
+
".execute(",
|
|
132
|
+
".commit(",
|
|
133
|
+
".rollback(",
|
|
134
|
+
".query(",
|
|
135
|
+
".add(",
|
|
136
|
+
".delete(",
|
|
137
|
+
".merge(",
|
|
138
|
+
".flush(",
|
|
139
|
+
]
|
|
140
|
+
):
|
|
141
|
+
self._add_sqlalchemy_errors()
|
|
142
|
+
|
|
143
|
+
# Pydantic validation
|
|
144
|
+
elif any(
|
|
145
|
+
pattern in call_str
|
|
146
|
+
for pattern in [
|
|
147
|
+
".model_validate(",
|
|
148
|
+
".parse_obj(",
|
|
149
|
+
".model_dump(",
|
|
150
|
+
".model_validate_json(",
|
|
151
|
+
]
|
|
152
|
+
):
|
|
153
|
+
self.errors.add("VALIDATION_FAILED")
|
|
154
|
+
|
|
155
|
+
# Async operations
|
|
156
|
+
elif "await " in call_str:
|
|
157
|
+
self.errors.update(["INTERNAL_ERROR", "TIMEOUT_ERROR"])
|
|
158
|
+
|
|
159
|
+
def _get_call_string(self, node: ast.Call) -> str:
|
|
160
|
+
"""Get string representation of call for pattern matching."""
|
|
161
|
+
try:
|
|
162
|
+
if isinstance(node.func, ast.Attribute):
|
|
163
|
+
return f"{self._get_attr_chain(node.func)}()"
|
|
164
|
+
elif isinstance(node.func, ast.Name):
|
|
165
|
+
return f"{node.func.id}()"
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
return ""
|
|
169
|
+
|
|
170
|
+
def _get_attr_chain(self, node: ast.Attribute) -> str:
|
|
171
|
+
"""Get full attribute chain like 'obj.method'."""
|
|
172
|
+
parts = []
|
|
173
|
+
current: ast.AST = node
|
|
174
|
+
|
|
175
|
+
while isinstance(current, ast.Attribute):
|
|
176
|
+
parts.append(current.attr)
|
|
177
|
+
current = current.value
|
|
178
|
+
|
|
179
|
+
if isinstance(current, ast.Name):
|
|
180
|
+
parts.append(current.id)
|
|
181
|
+
|
|
182
|
+
return ".".join(reversed(parts)) if parts else ""
|
|
183
|
+
|
|
184
|
+
def _extract_error_info(self, node: ast.AST) -> Optional[Dict[str, Any]]:
|
|
185
|
+
"""Extract error information from raise statement."""
|
|
186
|
+
if isinstance(node, ast.Call):
|
|
187
|
+
# Handle: raise SomeError("message", code="ERROR_CODE")
|
|
188
|
+
func_name = self._get_function_name(node.func)
|
|
189
|
+
|
|
190
|
+
if func_name and self._is_app_error_class(func_name):
|
|
191
|
+
if func_name == "HTTPException":
|
|
192
|
+
# Special handling for HTTPException
|
|
193
|
+
error_info = self._extract_http_exception_info(node)
|
|
194
|
+
else:
|
|
195
|
+
error_info = {
|
|
196
|
+
"type": func_name,
|
|
197
|
+
"code": self._extract_error_code(node),
|
|
198
|
+
"message": self._extract_error_message(node),
|
|
199
|
+
"line": getattr(node, "lineno", None),
|
|
200
|
+
}
|
|
201
|
+
return error_info
|
|
202
|
+
|
|
203
|
+
elif isinstance(node, ast.Name):
|
|
204
|
+
# Handle: raise existing_error
|
|
205
|
+
return {
|
|
206
|
+
"type": "unknown",
|
|
207
|
+
"code": "UNKNOWN_ERROR",
|
|
208
|
+
"message": "Re-raised error",
|
|
209
|
+
"line": getattr(node, "lineno", None),
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
def _extract_error_code(self, node: ast.Call) -> str:
|
|
215
|
+
"""Extract error code from exception constructor."""
|
|
216
|
+
# Look for code parameter
|
|
217
|
+
for keyword in node.keywords:
|
|
218
|
+
if keyword.arg == "code":
|
|
219
|
+
# Handle string constants
|
|
220
|
+
if isinstance(keyword.value, ast.Constant) and isinstance(
|
|
221
|
+
keyword.value.value, str
|
|
222
|
+
):
|
|
223
|
+
return keyword.value.value
|
|
224
|
+
# Handle ErrorCode.CONSTANT_NAME
|
|
225
|
+
elif isinstance(keyword.value, ast.Attribute):
|
|
226
|
+
if (
|
|
227
|
+
isinstance(keyword.value.value, ast.Name)
|
|
228
|
+
and keyword.value.value.id == "ErrorCode"
|
|
229
|
+
):
|
|
230
|
+
return keyword.value.attr
|
|
231
|
+
# Handle ErrorCode("CUSTOM_ERROR_CODE")
|
|
232
|
+
elif isinstance(keyword.value, ast.Call):
|
|
233
|
+
if (
|
|
234
|
+
isinstance(keyword.value.func, ast.Name)
|
|
235
|
+
and keyword.value.func.id == "ErrorCode"
|
|
236
|
+
and keyword.value.args
|
|
237
|
+
and isinstance(keyword.value.args[0], ast.Constant)
|
|
238
|
+
and isinstance(keyword.value.args[0].value, str)
|
|
239
|
+
):
|
|
240
|
+
return keyword.value.args[0].value
|
|
241
|
+
return "UNKNOWN_ERROR"
|
|
242
|
+
|
|
243
|
+
# Look for ErrorCode in positional args
|
|
244
|
+
for arg in node.args:
|
|
245
|
+
if isinstance(arg, ast.Call):
|
|
246
|
+
func_name = self._get_function_name(arg.func)
|
|
247
|
+
if func_name == "ErrorCode" and arg.args:
|
|
248
|
+
return self._extract_string_value(arg.args[0]) or "UNKNOWN_ERROR"
|
|
249
|
+
|
|
250
|
+
# Default based on exception type
|
|
251
|
+
func_name = self._get_function_name(node.func)
|
|
252
|
+
return self._get_default_error_code(func_name or "unknown")
|
|
253
|
+
|
|
254
|
+
def _extract_error_message(self, node: ast.Call) -> str:
|
|
255
|
+
"""Extract error message from exception constructor."""
|
|
256
|
+
if node.args:
|
|
257
|
+
message = self._extract_string_value(node.args[0])
|
|
258
|
+
if message:
|
|
259
|
+
return message
|
|
260
|
+
|
|
261
|
+
# Look for message parameter
|
|
262
|
+
for keyword in node.keywords:
|
|
263
|
+
if keyword.arg == "message":
|
|
264
|
+
return self._extract_string_value(keyword.value) or "Unknown error"
|
|
265
|
+
|
|
266
|
+
return "Unknown error"
|
|
267
|
+
|
|
268
|
+
def _extract_string_value(self, node: ast.AST) -> Optional[str]:
|
|
269
|
+
"""Extract string value from AST node."""
|
|
270
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
271
|
+
return node.value
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
def _get_function_name(self, node: ast.AST) -> Optional[str]:
|
|
275
|
+
"""Get function name from call node."""
|
|
276
|
+
if isinstance(node, ast.Name):
|
|
277
|
+
return node.id
|
|
278
|
+
elif isinstance(node, ast.Attribute):
|
|
279
|
+
return node.attr
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
def _is_app_error_class(self, name: str) -> bool:
|
|
283
|
+
"""Check if name is an AppError class."""
|
|
284
|
+
app_error_classes = {
|
|
285
|
+
"AppError",
|
|
286
|
+
"ValidationError",
|
|
287
|
+
"AuthError",
|
|
288
|
+
"NotFoundError",
|
|
289
|
+
"DatabaseError",
|
|
290
|
+
"BusinessLogicError",
|
|
291
|
+
"HTTPException", # Support FastAPI HTTPException
|
|
292
|
+
}
|
|
293
|
+
return name in app_error_classes
|
|
294
|
+
|
|
295
|
+
def _get_default_error_code(self, class_name: str) -> str:
|
|
296
|
+
"""Get default error code for exception class."""
|
|
297
|
+
default_codes = {
|
|
298
|
+
"ValidationError": "VALIDATION_FAILED",
|
|
299
|
+
"AuthError": "AUTH_REQUIRED",
|
|
300
|
+
"NotFoundError": "RESOURCE_NOT_FOUND",
|
|
301
|
+
"DatabaseError": "DB_QUERY_ERROR",
|
|
302
|
+
"BusinessLogicError": "BUSINESS_RULE_VIOLATION",
|
|
303
|
+
"AppError": "INTERNAL_ERROR",
|
|
304
|
+
"HTTPException": "HTTP_EXCEPTION",
|
|
305
|
+
}
|
|
306
|
+
return default_codes.get(class_name, "UNKNOWN_ERROR")
|
|
307
|
+
|
|
308
|
+
def _extract_http_exception_info(self, node: ast.Call) -> Dict[str, Any]:
|
|
309
|
+
"""Extract error information from HTTPException constructor."""
|
|
310
|
+
status_code = 500
|
|
311
|
+
message = "HTTP Error"
|
|
312
|
+
|
|
313
|
+
# Extract status_code from kwargs or positional args
|
|
314
|
+
for keyword in node.keywords:
|
|
315
|
+
if keyword.arg == "status_code":
|
|
316
|
+
if isinstance(keyword.value, ast.Constant) and isinstance(
|
|
317
|
+
keyword.value.value, int
|
|
318
|
+
):
|
|
319
|
+
status_code = keyword.value.value
|
|
320
|
+
elif keyword.arg == "detail":
|
|
321
|
+
if isinstance(keyword.value, ast.Constant) and isinstance(
|
|
322
|
+
keyword.value.value, str
|
|
323
|
+
):
|
|
324
|
+
message = keyword.value.value
|
|
325
|
+
|
|
326
|
+
# Check positional args for status_code
|
|
327
|
+
if (
|
|
328
|
+
node.args
|
|
329
|
+
and isinstance(node.args[0], ast.Constant)
|
|
330
|
+
and isinstance(node.args[0].value, int)
|
|
331
|
+
):
|
|
332
|
+
status_code = node.args[0].value
|
|
333
|
+
|
|
334
|
+
# Map status code to error code
|
|
335
|
+
error_code = self._map_status_code_to_error_code(status_code)
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
"type": "HTTPException",
|
|
339
|
+
"code": error_code,
|
|
340
|
+
"message": message,
|
|
341
|
+
"status_code": status_code,
|
|
342
|
+
"line": getattr(node, "lineno", None),
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
def _map_status_code_to_error_code(self, status_code: int) -> str:
|
|
346
|
+
"""Map HTTP status code to appropriate error code."""
|
|
347
|
+
status_to_error = {
|
|
348
|
+
400: "VALIDATION_FAILED",
|
|
349
|
+
401: "AUTH_REQUIRED",
|
|
350
|
+
403: "AUTH_PERMISSION_DENIED",
|
|
351
|
+
404: "RESOURCE_NOT_FOUND",
|
|
352
|
+
409: "RESOURCE_CONFLICT",
|
|
353
|
+
422: "BUSINESS_RULE_VIOLATION",
|
|
354
|
+
500: "INTERNAL_ERROR",
|
|
355
|
+
}
|
|
356
|
+
return status_to_error.get(status_code, "HTTP_EXCEPTION")
|
|
357
|
+
|
|
358
|
+
def _analyze_decorators(self, func: Callable) -> None:
|
|
359
|
+
"""Analyze function decorators for potential errors."""
|
|
360
|
+
try:
|
|
361
|
+
source_lines = inspect.getsourcelines(func)[0]
|
|
362
|
+
|
|
363
|
+
decorator_lines = []
|
|
364
|
+
for line in source_lines:
|
|
365
|
+
stripped = line.strip()
|
|
366
|
+
if stripped.startswith("@"):
|
|
367
|
+
decorator_lines.append(stripped)
|
|
368
|
+
elif stripped.startswith("def "):
|
|
369
|
+
break
|
|
370
|
+
|
|
371
|
+
for decorator_line in decorator_lines:
|
|
372
|
+
self._analyze_decorator_line(decorator_line)
|
|
373
|
+
|
|
374
|
+
except (OSError, TypeError):
|
|
375
|
+
pass
|
|
376
|
+
|
|
377
|
+
def _analyze_decorator_line(self, decorator_line: str) -> None:
|
|
378
|
+
"""Analyze a single decorator line."""
|
|
379
|
+
try:
|
|
380
|
+
decorator_name = decorator_line.replace("@", "").strip()
|
|
381
|
+
|
|
382
|
+
if "(" in decorator_name:
|
|
383
|
+
decorator_name = decorator_name.split("(")[0]
|
|
384
|
+
|
|
385
|
+
# Common decorator patterns
|
|
386
|
+
decorator_errors = {
|
|
387
|
+
"require_auth": ["AUTH_REQUIRED", "AUTH_PERMISSION_DENIED"],
|
|
388
|
+
"validate_input": ["VALIDATION_FAILED", "INVALID_INPUT"],
|
|
389
|
+
"rate_limit": ["RATE_LIMIT_EXCEEDED"],
|
|
390
|
+
"cache": ["CACHE_ERROR"],
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if decorator_name in decorator_errors:
|
|
394
|
+
errors = decorator_errors[decorator_name]
|
|
395
|
+
self.errors.update(errors)
|
|
396
|
+
self.decorator_errors.append(
|
|
397
|
+
{
|
|
398
|
+
"decorator": decorator_name,
|
|
399
|
+
"possible_errors": errors,
|
|
400
|
+
"type": "decorator_analysis",
|
|
401
|
+
}
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
except Exception:
|
|
405
|
+
pass
|
|
406
|
+
|
|
407
|
+
def _analyze_method_call(self, node: ast.Call) -> None:
|
|
408
|
+
"""Analyze method calls by trying to resolve and analyze the actual method."""
|
|
409
|
+
if isinstance(node.func, ast.Attribute):
|
|
410
|
+
# Try to resolve the actual method and analyze it
|
|
411
|
+
resolved_method = self._resolve_method_call(node)
|
|
412
|
+
if resolved_method and self.current_depth < self.max_depth:
|
|
413
|
+
self._analyze_function(resolved_method)
|
|
414
|
+
|
|
415
|
+
def _analyze_builtin_function(self, func: Callable) -> None:
|
|
416
|
+
"""Analyze built-in functions for common error patterns using existing converters."""
|
|
417
|
+
func_name = getattr(func, "__name__", str(func))
|
|
418
|
+
module_name = getattr(func, "__module__", "")
|
|
419
|
+
|
|
420
|
+
# SQLAlchemy errors - use existing SQL converter knowledge
|
|
421
|
+
if "sqlalchemy" in module_name.lower():
|
|
422
|
+
self._add_sqlalchemy_errors()
|
|
423
|
+
|
|
424
|
+
# JSON errors
|
|
425
|
+
elif func_name in ["loads", "dumps"] and "json" in module_name:
|
|
426
|
+
self.errors.add("INVALID_FORMAT")
|
|
427
|
+
|
|
428
|
+
# HTTP client errors
|
|
429
|
+
elif "requests" in module_name or "httpx" in module_name:
|
|
430
|
+
self.errors.update(["INTERNAL_ERROR", "NETWORK_ERROR"])
|
|
431
|
+
|
|
432
|
+
def _add_sqlalchemy_errors(self) -> None:
|
|
433
|
+
"""Add SQLAlchemy errors using existing converter knowledge."""
|
|
434
|
+
from ..converters.sql_converter import SQLErrorConverter
|
|
435
|
+
from ..core.error_codes import ErrorCode
|
|
436
|
+
|
|
437
|
+
# Get all possible error codes from SQL converter patterns
|
|
438
|
+
sql_error_codes = set()
|
|
439
|
+
for pattern, (code, message) in SQLErrorConverter.SQL_PATTERNS.items():
|
|
440
|
+
sql_error_codes.add(code.value)
|
|
441
|
+
|
|
442
|
+
# Add common SQLAlchemy errors based on existing converter
|
|
443
|
+
sql_error_codes.update(
|
|
444
|
+
[
|
|
445
|
+
ErrorCode.DB_CONNECTION_ERROR.value,
|
|
446
|
+
ErrorCode.DB_QUERY_ERROR.value,
|
|
447
|
+
ErrorCode.DB_TRANSACTION_ERROR.value,
|
|
448
|
+
]
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
self.errors.update(sql_error_codes)
|
|
452
|
+
|
|
453
|
+
def _resolve_method_call(self, node: ast.Call) -> Optional[Callable]:
|
|
454
|
+
"""Try to resolve method call to actual method object."""
|
|
455
|
+
# For now, return None but analyze based on context
|
|
456
|
+
# This is complex and would require runtime introspection
|
|
457
|
+
return None
|
|
458
|
+
|
|
459
|
+
def _resolve_function_call(self, node: ast.Call) -> Optional[Callable]:
|
|
460
|
+
"""Try to resolve function call to actual function object."""
|
|
461
|
+
# For now, return None but analyze based on context
|
|
462
|
+
# This is complex and would require runtime introspection
|
|
463
|
+
return None
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any
|
|
2
|
+
from ..core.error_response import ErrorDetail
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BackendError(Exception):
|
|
6
|
+
"""
|
|
7
|
+
Client-side exception for handling backend API errors.
|
|
8
|
+
|
|
9
|
+
This exception is raised when the client receives an error response
|
|
10
|
+
from the backend API. It wraps the error details from the server
|
|
11
|
+
and provides convenient properties for error handling.
|
|
12
|
+
|
|
13
|
+
Unlike core exceptions (AppError, ValidationError, etc.), this is used
|
|
14
|
+
on the client side to handle errors received from the server.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
try:
|
|
18
|
+
response = api_client.get_user(123)
|
|
19
|
+
except BackendError as e:
|
|
20
|
+
if e.is_not_found_error():
|
|
21
|
+
print("User not found")
|
|
22
|
+
elif e.is_validation_error():
|
|
23
|
+
print("Invalid input")
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
error: ErrorDetail,
|
|
29
|
+
status_code: int,
|
|
30
|
+
response_headers: Optional[Dict[str, str]] = None,
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Initialize backend error.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
error: Error detail model from server response
|
|
37
|
+
status_code: HTTP status code from response
|
|
38
|
+
response_headers: Response headers for additional context
|
|
39
|
+
"""
|
|
40
|
+
self.error = error
|
|
41
|
+
self.status_code = status_code
|
|
42
|
+
self.response_headers = response_headers or {}
|
|
43
|
+
|
|
44
|
+
super().__init__(error.message)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def code(self) -> str:
|
|
48
|
+
"""Get error code from server response."""
|
|
49
|
+
return self.error.code
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def message(self) -> str:
|
|
53
|
+
"""Get error message from server response."""
|
|
54
|
+
return self.error.message
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def details(self) -> Dict[str, Any]:
|
|
58
|
+
"""Get error details from server response."""
|
|
59
|
+
return self.error.details
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def request_id(self) -> str:
|
|
63
|
+
"""Get request ID for tracing."""
|
|
64
|
+
return self.error.request_id
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def timestamp(self):
|
|
68
|
+
"""Get error timestamp from server."""
|
|
69
|
+
return self.error.timestamp
|
|
70
|
+
|
|
71
|
+
def is_validation_error(self) -> bool:
|
|
72
|
+
"""Check if this is a validation error from server."""
|
|
73
|
+
return self.code in [
|
|
74
|
+
"VALIDATION_FAILED",
|
|
75
|
+
"INVALID_INPUT",
|
|
76
|
+
"MISSING_REQUIRED_FIELD",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
def is_auth_error(self) -> bool:
|
|
80
|
+
"""Check if this is an authentication/authorization error from server."""
|
|
81
|
+
return self.code.startswith("AUTH_")
|
|
82
|
+
|
|
83
|
+
def is_not_found_error(self) -> bool:
|
|
84
|
+
"""Check if this is a not found error from server."""
|
|
85
|
+
return self.code.endswith("_NOT_FOUND")
|
|
86
|
+
|
|
87
|
+
def is_database_error(self) -> bool:
|
|
88
|
+
"""Check if this is a database error from server."""
|
|
89
|
+
return self.code.startswith("DB_")
|
|
90
|
+
|
|
91
|
+
def is_business_error(self) -> bool:
|
|
92
|
+
"""Check if this is a business logic error from server."""
|
|
93
|
+
return self.code in [
|
|
94
|
+
"BUSINESS_RULE_VIOLATION",
|
|
95
|
+
"INSUFFICIENT_BALANCE",
|
|
96
|
+
"OPERATION_NOT_ALLOWED",
|
|
97
|
+
]
|