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,231 @@
1
+ """WebSocket error handler for awesome-errors"""
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ from typing import Dict, Any, Type, Callable, Optional
7
+ from fastapi import FastAPI, WebSocket
8
+
9
+ from awesome_errors.core.error_codes import ErrorCode
10
+ from awesome_errors.core.exceptions import AppError
11
+ from awesome_errors.websocket.exceptions import WebSocketError, WebSocketInternalError
12
+ from starlette.websockets import WebSocketState
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class WebSocketErrorHandler:
18
+ """
19
+ Centralized error handler for WebSocket connections
20
+
21
+ Provides automatic error conversion, logging, and connection management
22
+ similar to Anaya's approach but for WebSocket.
23
+ """
24
+
25
+ def __init__(self):
26
+ self.error_mappings: Dict[Type[Exception], Dict[str, Any]] = {}
27
+ self._setup_default_mappings()
28
+
29
+ def _setup_default_mappings(self):
30
+ """Setup default error mappings"""
31
+ from .exceptions import (
32
+ WebSocketValidationError,
33
+ WebSocketInternalError,
34
+ JSONRPCErrorCode,
35
+ )
36
+
37
+ # Map common exceptions to WebSocket errors
38
+ self.error_mappings.update(
39
+ {
40
+ # Pydantic validation
41
+ ValueError: {
42
+ "converter": lambda e: WebSocketValidationError(
43
+ message=str(e), request_id=None
44
+ )
45
+ },
46
+ # JSON parsing
47
+ json.JSONDecodeError: {
48
+ "converter": lambda e: WebSocketError(
49
+ code=ErrorCode.INVALID_FORMAT,
50
+ message="Invalid JSON",
51
+ ws_error_code=JSONRPCErrorCode.PARSE_ERROR,
52
+ request_id=None,
53
+ )
54
+ },
55
+ # Generic exceptions
56
+ Exception: {
57
+ "converter": lambda e: WebSocketInternalError(
58
+ message="Internal server error",
59
+ original_error=str(e),
60
+ request_id=None,
61
+ )
62
+ },
63
+ }
64
+ )
65
+
66
+ def register_error_mapping(
67
+ self,
68
+ error_type: Type[Exception],
69
+ converter: Callable[[Exception], WebSocketError],
70
+ ):
71
+ """Register custom error mapping"""
72
+ self.error_mappings[error_type] = {"converter": converter}
73
+
74
+ async def handle_websocket_error(
75
+ self, websocket: WebSocket, error: Exception, request_id: Optional[str] = None
76
+ ) -> bool:
77
+ """
78
+ Handle WebSocket error
79
+
80
+ Returns:
81
+ bool: True if connection should be closed
82
+ """
83
+ try:
84
+ # Convert to WebSocketError
85
+ ws_error = self._convert_to_websocket_error(error, request_id)
86
+
87
+ # Log the error
88
+ self._log_error(ws_error)
89
+
90
+ # Send error response
91
+ await self._send_error_response(websocket, ws_error)
92
+
93
+ # Handle connection closing if needed
94
+ if ws_error.close_connection:
95
+ await self._close_connection(websocket, ws_error)
96
+ return True
97
+
98
+ return False
99
+
100
+ except Exception as handler_error:
101
+ logger.error(f"Error in error handler: {handler_error}")
102
+ # Try to close connection gracefully
103
+ try:
104
+ await websocket.close(code=1011, reason="Error handler failed")
105
+ except Exception:
106
+ pass
107
+ return True
108
+
109
+ def _convert_to_websocket_error(
110
+ self, error: Exception, request_id: Optional[str] = None
111
+ ) -> WebSocketError:
112
+ """Convert any exception to WebSocketError"""
113
+
114
+ # If already a WebSocketError, just update request_id
115
+ if isinstance(error, WebSocketError):
116
+ if request_id and not error.request_id:
117
+ error.request_id = request_id
118
+ return error
119
+
120
+ # If it's an AppError, convert it
121
+ if isinstance(error, AppError):
122
+ return WebSocketError.from_app_error(error, request_id)
123
+
124
+ # Check registered mappings
125
+ for error_type, mapping in self.error_mappings.items():
126
+ if isinstance(error, error_type):
127
+ ws_error = mapping["converter"](error)
128
+ if request_id and not ws_error.request_id:
129
+ ws_error.request_id = request_id
130
+ return ws_error
131
+
132
+ # Fallback to internal error
133
+ return WebSocketInternalError(
134
+ message="Unexpected error occurred",
135
+ original_error=str(error),
136
+ request_id=request_id,
137
+ )
138
+
139
+ def _log_error(self, error: WebSocketError):
140
+ """Log error based on severity"""
141
+ if error.ws_error_code < -32600: # Standard JSON-RPC errors
142
+ logger.error(
143
+ f"WebSocket error: {error.message}",
144
+ extra={
145
+ "error_code": error.code.value,
146
+ "ws_error_code": error.ws_error_code,
147
+ "request_id": error.request_id,
148
+ },
149
+ )
150
+ else: # Custom errors
151
+ logger.warning(
152
+ f"WebSocket error: {error.message}",
153
+ extra={
154
+ "error_code": error.code.value,
155
+ "ws_error_code": error.ws_error_code,
156
+ "request_id": error.request_id,
157
+ },
158
+ )
159
+
160
+ async def _send_error_response(self, websocket: WebSocket, error: WebSocketError):
161
+ """Send error response via WebSocket"""
162
+ try:
163
+ if getattr(websocket, "client_state", None) not in (
164
+ WebSocketState.CONNECTED,
165
+ ):
166
+ return
167
+
168
+ error_response = json.dumps(error.to_jsonrpc_error())
169
+ await websocket.send_text(error_response)
170
+ except Exception as send_error:
171
+ logger.error(f"Failed to send error response: {send_error}")
172
+
173
+ async def _close_connection(self, websocket: WebSocket, error: WebSocketError):
174
+ """Close WebSocket connection with proper code and reason"""
175
+ try:
176
+ # Wait a bit to ensure error message is delivered
177
+ await asyncio.sleep(0.1)
178
+
179
+ # Map error codes to WebSocket close codes
180
+ close_code = self._get_close_code(error)
181
+ close_reason = error.message[:123] # Max 123 bytes for close reason
182
+
183
+ await websocket.close(code=close_code, reason=close_reason)
184
+ if getattr(websocket, "client_state", None) not in (
185
+ WebSocketState.CONNECTED,
186
+ ):
187
+ await websocket.close(code=close_code, reason=close_reason)
188
+
189
+ except Exception as close_error:
190
+ logger.error(f"Failed to close WebSocket: {close_error}")
191
+
192
+ def _get_close_code(self, error: WebSocketError) -> int:
193
+ """Get appropriate WebSocket close code"""
194
+ from .exceptions import JSONRPCErrorCode
195
+
196
+ # Map error codes to WebSocket close codes
197
+ close_code_mapping = {
198
+ JSONRPCErrorCode.AUTH_REQUIRED: 1008, # Policy Violation
199
+ JSONRPCErrorCode.TOKEN_EXPIRED: 1008, # Policy Violation
200
+ JSONRPCErrorCode.RATE_LIMITED: 1008, # Policy Violation
201
+ JSONRPCErrorCode.INTERNAL_ERROR: 1011, # Internal Error
202
+ JSONRPCErrorCode.INVALID_REQUEST: 1003, # Unsupported Data
203
+ JSONRPCErrorCode.PARSE_ERROR: 1007, # Invalid frame payload data
204
+ }
205
+
206
+ return close_code_mapping.get(error.ws_error_code, 1000) # Normal closure
207
+
208
+
209
+ def setup_websocket_error_handling(app: FastAPI) -> WebSocketErrorHandler:
210
+ """
211
+ Setup WebSocket error handling for FastAPI app
212
+
213
+ Usage:
214
+ ws_error_handler = setup_websocket_error_handling(app)
215
+
216
+ # In WebSocket endpoint:
217
+ try:
218
+ # ... handle message
219
+ except Exception as e:
220
+ should_close = await ws_error_handler.handle_websocket_error(
221
+ websocket, e, request_id
222
+ )
223
+ if should_close:
224
+ break
225
+ """
226
+ handler = WebSocketErrorHandler()
227
+
228
+ # Store handler in app state for access
229
+ app.state.ws_error_handler = handler
230
+
231
+ return handler
@@ -0,0 +1,286 @@
1
+ """
2
+ WebSocket exceptions extending awesome-errors for JSON-RPC 2.0 compatibility
3
+
4
+ This module provides WebSocket-specific error classes that integrate with
5
+ the existing awesome-errors system while adding JSON-RPC 2.0 support.
6
+ """
7
+
8
+ from typing import Any, Dict, Optional
9
+
10
+ from awesome_errors.core.error_codes import ErrorCode
11
+ from awesome_errors.core.exceptions import AppError
12
+
13
+
14
+ class JSONRPCErrorCode:
15
+ """Standard JSON-RPC 2.0 error codes"""
16
+
17
+ # Standard codes defined by JSON-RPC 2.0 specification
18
+ PARSE_ERROR = -32700 # Invalid JSON was received
19
+ INVALID_REQUEST = -32600 # The JSON sent is not a valid Request object
20
+ METHOD_NOT_FOUND = -32601 # The method does not exist / is not available
21
+ INVALID_PARAMS = -32602 # Invalid method parameter(s)
22
+ INTERNAL_ERROR = -32603 # Internal JSON-RPC error
23
+
24
+ # Server error codes (reserved range: -32000 to -32099)
25
+ # Custom codes for WebSocket-specific errors
26
+ AUTH_REQUIRED = -32000 # Authentication required
27
+ TOKEN_EXPIRED = -32001 # Authentication token expired
28
+ RATE_LIMITED = -32002 # Rate limit exceeded
29
+ SUBSCRIPTION_DENIED = -32003 # Subscription permission denied
30
+ CONNECTION_LIMIT = -32004 # Too many connections
31
+
32
+
33
+ class WebSocketError(AppError):
34
+ """
35
+ WebSocket error extending AppError with JSON-RPC 2.0 compatibility
36
+
37
+ This class bridges awesome-errors with WebSocket/JSON-RPC requirements,
38
+ providing automatic error code mapping and connection management.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ code: ErrorCode,
44
+ message: str,
45
+ ws_error_code: Optional[int] = None,
46
+ request_id: Optional[str] = None,
47
+ details: Optional[Dict[str, Any]] = None,
48
+ close_connection: bool = False,
49
+ ):
50
+ """
51
+ Initialize WebSocket error
52
+
53
+ Args:
54
+ code: ErrorCode from awesome-errors
55
+ message: Human-readable error message
56
+ ws_error_code: JSON-RPC error code (auto-mapped if not provided)
57
+ request_id: JSON-RPC request ID for correlation
58
+ details: Additional error details
59
+ close_connection: Whether to close WebSocket connection after error
60
+ """
61
+ super().__init__(code, message, details)
62
+
63
+ # WebSocket specific fields
64
+ self.ws_error_code = ws_error_code or self._map_to_jsonrpc_code(code)
65
+ self.request_id = request_id
66
+ self.close_connection = close_connection
67
+
68
+ def _map_to_jsonrpc_code(self, error_code: ErrorCode) -> int:
69
+ """
70
+ Map ErrorCode to JSON-RPC error code
71
+
72
+ Provides sensible defaults for mapping awesome-errors codes
73
+ to JSON-RPC 2.0 standard codes.
74
+ """
75
+ mapping = {
76
+ # Authentication errors
77
+ ErrorCode.AUTH_REQUIRED: JSONRPCErrorCode.AUTH_REQUIRED,
78
+ ErrorCode.AUTH_PERMISSION_DENIED: JSONRPCErrorCode.AUTH_REQUIRED,
79
+ ErrorCode.AUTH_TOKEN_EXPIRED: JSONRPCErrorCode.TOKEN_EXPIRED,
80
+ ErrorCode.AUTH_INVALID_TOKEN: JSONRPCErrorCode.AUTH_REQUIRED,
81
+ # Validation errors
82
+ ErrorCode.VALIDATION_FAILED: JSONRPCErrorCode.INVALID_PARAMS,
83
+ ErrorCode.INVALID_INPUT: JSONRPCErrorCode.INVALID_PARAMS,
84
+ ErrorCode.MISSING_REQUIRED_FIELD: JSONRPCErrorCode.INVALID_PARAMS,
85
+ # Resource errors
86
+ ErrorCode.RESOURCE_NOT_FOUND: JSONRPCErrorCode.METHOD_NOT_FOUND,
87
+ ErrorCode.RESOURCE_ALREADY_EXISTS: JSONRPCErrorCode.INVALID_REQUEST,
88
+ # Rate limiting
89
+ ErrorCode.RATE_LIMIT_EXCEEDED: JSONRPCErrorCode.RATE_LIMITED,
90
+ # Database errors (all map to internal error)
91
+ ErrorCode.DB_CONNECTION_ERROR: JSONRPCErrorCode.INTERNAL_ERROR,
92
+ ErrorCode.DB_QUERY_ERROR: JSONRPCErrorCode.INTERNAL_ERROR,
93
+ ErrorCode.DB_CONSTRAINT_VIOLATION: JSONRPCErrorCode.INVALID_PARAMS,
94
+ # Business logic
95
+ ErrorCode.BUSINESS_RULE_VIOLATION: JSONRPCErrorCode.INVALID_REQUEST,
96
+ # Generic errors
97
+ ErrorCode.INTERNAL_ERROR: JSONRPCErrorCode.INTERNAL_ERROR,
98
+ ErrorCode.SERVICE_UNAVAILABLE: JSONRPCErrorCode.INTERNAL_ERROR,
99
+ }
100
+
101
+ return mapping.get(error_code, JSONRPCErrorCode.INTERNAL_ERROR)
102
+
103
+ def to_jsonrpc_error(self) -> Dict[str, Any]:
104
+ """
105
+ Convert to JSON-RPC 2.0 error format
106
+
107
+ Returns a dictionary conforming to JSON-RPC 2.0 error object specification.
108
+ """
109
+ error_data: Dict[str, Any] | None = None
110
+
111
+ # Build error data if we have details or metadata
112
+ if self.details or self.request_id:
113
+ error_data = {}
114
+
115
+ if self.details:
116
+ error_data["details"] = self.details
117
+
118
+ # Include original error code for clients that understand it
119
+ error_data["error_code"] = self.code.value
120
+
121
+ # Include timestamp for debugging
122
+ error_data["timestamp"] = self.timestamp.isoformat() + "Z"
123
+
124
+ # Include request ID if available
125
+ if self.request_id:
126
+ error_data["request_id"] = self.request_id
127
+
128
+ return {
129
+ "jsonrpc": "2.0",
130
+ "error": {
131
+ "code": self.ws_error_code,
132
+ "message": self.message,
133
+ "data": error_data,
134
+ },
135
+ "id": self.request_id,
136
+ }
137
+
138
+ @classmethod
139
+ def from_app_error(
140
+ cls,
141
+ error: AppError,
142
+ request_id: Optional[str] = None,
143
+ close_connection: bool = False,
144
+ ) -> "WebSocketError":
145
+ """
146
+ Create WebSocketError from existing AppError
147
+
148
+ This allows seamless conversion of any AppError to WebSocket format.
149
+ """
150
+ return cls(
151
+ code=error.code,
152
+ message=error.message,
153
+ details=error.details,
154
+ request_id=request_id,
155
+ close_connection=close_connection,
156
+ )
157
+
158
+
159
+ class WebSocketAuthError(WebSocketError):
160
+ """Authentication error - always closes connection"""
161
+
162
+ def __init__(
163
+ self,
164
+ message: str = "Authentication required",
165
+ request_id: Optional[str] = None,
166
+ details: Optional[Dict[str, Any]] = None,
167
+ ):
168
+ super().__init__(
169
+ code=ErrorCode.AUTH_REQUIRED,
170
+ message=message,
171
+ ws_error_code=JSONRPCErrorCode.AUTH_REQUIRED,
172
+ request_id=request_id,
173
+ details=details,
174
+ close_connection=True, # Always close on auth failure
175
+ )
176
+
177
+
178
+ class WebSocketTokenExpiredError(WebSocketError):
179
+ """Token expired error - requires re-authentication"""
180
+
181
+ def __init__(
182
+ self, request_id: Optional[str] = None, details: Optional[Dict[str, Any]] = None
183
+ ):
184
+ super().__init__(
185
+ code=ErrorCode.AUTH_TOKEN_EXPIRED,
186
+ message="Token expired",
187
+ ws_error_code=JSONRPCErrorCode.TOKEN_EXPIRED,
188
+ request_id=request_id,
189
+ details=details,
190
+ close_connection=True, # Force reconnection with new token
191
+ )
192
+
193
+
194
+ class WebSocketRateLimitError(WebSocketError):
195
+ """Rate limit exceeded error"""
196
+
197
+ def __init__(
198
+ self,
199
+ retry_after: int,
200
+ request_id: Optional[str] = None,
201
+ limit: Optional[int] = None,
202
+ window: Optional[int] = None,
203
+ ):
204
+ details: Dict[str, Any] = {
205
+ "retry_after": retry_after,
206
+ "limit": limit,
207
+ "window": window,
208
+ }
209
+
210
+ super().__init__(
211
+ code=ErrorCode.RATE_LIMIT_EXCEEDED,
212
+ message=f"Rate limit exceeded. Retry after {retry_after} seconds",
213
+ ws_error_code=JSONRPCErrorCode.RATE_LIMITED,
214
+ details={k: v for k, v in details.items() if v is not None},
215
+ request_id=request_id,
216
+ close_connection=False, # Don't close, client can retry
217
+ )
218
+
219
+
220
+ class WebSocketValidationError(WebSocketError):
221
+ """Validation error with field details"""
222
+
223
+ def __init__(
224
+ self,
225
+ message: str,
226
+ validation_errors: Optional[list[Any]] = None,
227
+ request_id: Optional[str] = None,
228
+ ):
229
+ details: Dict[str, Any] = {}
230
+ if validation_errors:
231
+ details["validation_errors"] = validation_errors
232
+
233
+ super().__init__(
234
+ code=ErrorCode.VALIDATION_FAILED,
235
+ message=message,
236
+ ws_error_code=JSONRPCErrorCode.INVALID_PARAMS,
237
+ details=details if details else None,
238
+ request_id=request_id,
239
+ close_connection=False,
240
+ )
241
+
242
+
243
+ class WebSocketMethodNotFoundError(WebSocketError):
244
+ """Method not found error"""
245
+
246
+ def __init__(
247
+ self,
248
+ method: str,
249
+ request_id: Optional[str] = None,
250
+ available_methods: Optional[list[str]] = None,
251
+ ):
252
+ details: Dict[str, Any] = {"method": method}
253
+ if available_methods:
254
+ details["available_methods"] = available_methods
255
+
256
+ super().__init__(
257
+ code=ErrorCode.RESOURCE_NOT_FOUND,
258
+ message=f"Method not found: {method}",
259
+ ws_error_code=JSONRPCErrorCode.METHOD_NOT_FOUND,
260
+ details=details,
261
+ request_id=request_id,
262
+ close_connection=False,
263
+ )
264
+
265
+
266
+ class WebSocketInternalError(WebSocketError):
267
+ """Internal server error"""
268
+
269
+ def __init__(
270
+ self,
271
+ message: str = "Internal server error",
272
+ request_id: Optional[str] = None,
273
+ original_error: Optional[str] = None,
274
+ ):
275
+ details: Dict[str, Any] = {}
276
+ if original_error:
277
+ details["original_error"] = str(original_error)
278
+
279
+ super().__init__(
280
+ code=ErrorCode.INTERNAL_ERROR,
281
+ message=message,
282
+ ws_error_code=JSONRPCErrorCode.INTERNAL_ERROR,
283
+ details=details if details else None,
284
+ request_id=request_id,
285
+ close_connection=False,
286
+ )