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,232 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Dict
4
+
5
+ from .analysis import ErrorAnalyzer, analyze_errors, openapi_errors
6
+ from .client import BackendError, ErrorResponseParser
7
+ from .converters import (
8
+ PydanticErrorConverter,
9
+ PythonErrorConverter,
10
+ SQLErrorConverter,
11
+ UniversalErrorConverter,
12
+ )
13
+ from .core.error_codes import ErrorCode
14
+ from .core.error_response import ErrorDetail, ErrorResponse
15
+ from .core.exceptions import (
16
+ APIError,
17
+ AppError,
18
+ AuthError,
19
+ AuthInsufficientPrivilegesError,
20
+ AuthInvalidTokenError,
21
+ AuthPermissionDeniedError,
22
+ AuthRequiredError,
23
+ AuthTokenExpiredError,
24
+ BusinessLogicError,
25
+ DatabaseConnectionError,
26
+ DatabaseConstraintViolationError,
27
+ DatabaseDuplicateEntryError,
28
+ DatabaseError,
29
+ DatabaseInvalidReferenceError,
30
+ DatabaseMissingRequiredError,
31
+ DatabaseQueryError,
32
+ DatabaseTransactionError,
33
+ EntityNotFoundError,
34
+ InsufficientBalanceError,
35
+ InvalidFormatError,
36
+ InvalidInputError,
37
+ MissingRequiredFieldError,
38
+ NotFoundError,
39
+ OAuthProviderUnknownError,
40
+ OperationNotAllowedError,
41
+ RefreshTokenReuseDetectedError,
42
+ ResourceNotFoundError,
43
+ SessionExpiredError,
44
+ UserNotFoundError,
45
+ ValidationError,
46
+ )
47
+ from .core.renderers import ErrorResponseFormat, ErrorResponseRenderer
48
+ from .i18n.translator import ErrorTranslator
49
+ from .litestar_utils import apply_api_errors, errors, raises_from
50
+ from .middleware.litestar import (
51
+ apply_litestar_openapi_problem_details,
52
+ create_litestar_exception_handlers,
53
+ )
54
+
55
+ _setup_error_handling: Callable[..., Any] | None = None
56
+ try: # pragma: no cover - optional dependency
57
+ from .middleware.fastapi import setup_error_handling as _setup_error_handling_impl
58
+ except ImportError: # pragma: no cover
59
+ pass
60
+ else:
61
+ _setup_error_handling = _setup_error_handling_impl
62
+
63
+ _setup_automatic_error_docs: Callable[..., Any] | None = None
64
+ _apply_auto_error_docs_to_router: Callable[..., Any] | None = None
65
+ _auto_analyze_errors: Callable[..., Any] | None = None
66
+ try: # pragma: no cover - optional dependency
67
+ from .integrations.fastapi_auto_docs import (
68
+ apply_auto_error_docs_to_router as _apply_auto_error_docs_to_router_impl,
69
+ )
70
+ from .integrations.fastapi_auto_docs import (
71
+ auto_analyze_errors as _auto_analyze_errors_impl,
72
+ )
73
+ from .integrations.fastapi_auto_docs import (
74
+ setup_automatic_error_docs as _setup_automatic_error_docs_impl,
75
+ )
76
+ except ImportError: # pragma: no cover
77
+ pass
78
+ else:
79
+ _setup_automatic_error_docs = _setup_automatic_error_docs_impl
80
+ _apply_auto_error_docs_to_router = _apply_auto_error_docs_to_router_impl
81
+ _auto_analyze_errors = _auto_analyze_errors_impl
82
+
83
+ _websocket: Any = None
84
+ try: # pragma: no cover - optional dependency
85
+ from . import websocket as _websocket_module
86
+ except ImportError: # pragma: no cover
87
+ pass
88
+ else:
89
+ _websocket = _websocket_module
90
+
91
+ WebSocketError = getattr(_websocket, "WebSocketError", None)
92
+ JSONRPCErrorCode = getattr(_websocket, "JSONRPCErrorCode", None)
93
+ WebSocketAuthError = getattr(_websocket, "WebSocketAuthError", None)
94
+ WebSocketTokenExpiredError = getattr(_websocket, "WebSocketTokenExpiredError", None)
95
+ WebSocketRateLimitError = getattr(_websocket, "WebSocketRateLimitError", None)
96
+ WebSocketValidationError = getattr(_websocket, "WebSocketValidationError", None)
97
+ WebSocketMethodNotFoundError = getattr(_websocket, "WebSocketMethodNotFoundError", None)
98
+ WebSocketInternalError = getattr(_websocket, "WebSocketInternalError", None)
99
+ WebSocketErrorHandler = getattr(_websocket, "WebSocketErrorHandler", None)
100
+ _setup_websocket_error_handling = getattr(
101
+ _websocket, "setup_websocket_error_handling", None
102
+ )
103
+
104
+
105
+ def setup_error_handling(
106
+ app: Any,
107
+ translator: ErrorTranslator | None = None,
108
+ debug: bool = False,
109
+ log_errors: bool = True,
110
+ locales_dir: str | None = None,
111
+ default_locale: str = "en",
112
+ custom_translations: Dict[str, Dict[str, str]] | None = None,
113
+ response_format: ErrorResponseFormat = ErrorResponseFormat.LEGACY,
114
+ problem_type_resolver: Callable[[AppError], str] | None = None,
115
+ problem_extension_builder: Callable[[AppError], Dict[str, object]] | None = None,
116
+ message_resolver: Callable[
117
+ [AppError, str | None, ErrorTranslator | None], str
118
+ ] | None = None,
119
+ ) -> Any:
120
+ if _setup_error_handling is None:
121
+ raise ImportError(
122
+ "Install 'awesome-errors[fastapi]' to enable FastAPI middleware integration."
123
+ )
124
+ return _setup_error_handling(
125
+ app=app,
126
+ translator=translator,
127
+ debug=debug,
128
+ log_errors=log_errors,
129
+ locales_dir=locales_dir,
130
+ default_locale=default_locale,
131
+ custom_translations=custom_translations,
132
+ response_format=response_format,
133
+ problem_type_resolver=problem_type_resolver,
134
+ problem_extension_builder=problem_extension_builder,
135
+ message_resolver=message_resolver,
136
+ )
137
+
138
+
139
+ def setup_automatic_error_docs(app: Any, **kwargs: Any) -> Any:
140
+ if _setup_automatic_error_docs is None:
141
+ raise ImportError("FastAPI integration requires fastapi to be installed")
142
+ return _setup_automatic_error_docs(app, **kwargs)
143
+
144
+
145
+ def apply_auto_error_docs_to_router(router: Any, **kwargs: Any) -> Any:
146
+ if _apply_auto_error_docs_to_router is None:
147
+ raise ImportError("FastAPI integration requires fastapi to be installed")
148
+ return _apply_auto_error_docs_to_router(router, **kwargs)
149
+
150
+
151
+ def auto_analyze_errors(func: Any) -> Any:
152
+ if _auto_analyze_errors is None:
153
+ raise ImportError("FastAPI integration requires fastapi to be installed")
154
+ return _auto_analyze_errors(func)
155
+
156
+
157
+ def setup_websocket_error_handling(app: Any) -> Any:
158
+ if _setup_websocket_error_handling is None:
159
+ raise ImportError(
160
+ "Install 'awesome-errors[fastapi]' to enable WebSocket error handling."
161
+ )
162
+ return _setup_websocket_error_handling(app)
163
+
164
+
165
+ __version__ = "0.1.0"
166
+
167
+ __all__ = [
168
+ "AppError",
169
+ "APIError",
170
+ "ValidationError",
171
+ "InvalidInputError",
172
+ "MissingRequiredFieldError",
173
+ "InvalidFormatError",
174
+ "AuthError",
175
+ "AuthRequiredError",
176
+ "AuthInvalidTokenError",
177
+ "AuthTokenExpiredError",
178
+ "AuthPermissionDeniedError",
179
+ "AuthInsufficientPrivilegesError",
180
+ "SessionExpiredError",
181
+ "RefreshTokenReuseDetectedError",
182
+ "NotFoundError",
183
+ "ResourceNotFoundError",
184
+ "UserNotFoundError",
185
+ "EntityNotFoundError",
186
+ "OAuthProviderUnknownError",
187
+ "DatabaseError",
188
+ "DatabaseConnectionError",
189
+ "DatabaseQueryError",
190
+ "DatabaseTransactionError",
191
+ "DatabaseConstraintViolationError",
192
+ "DatabaseDuplicateEntryError",
193
+ "DatabaseInvalidReferenceError",
194
+ "DatabaseMissingRequiredError",
195
+ "BusinessLogicError",
196
+ "InsufficientBalanceError",
197
+ "OperationNotAllowedError",
198
+ "ErrorCode",
199
+ "ErrorResponse",
200
+ "ErrorDetail",
201
+ "ErrorResponseFormat",
202
+ "ErrorResponseRenderer",
203
+ "SQLErrorConverter",
204
+ "PythonErrorConverter",
205
+ "PydanticErrorConverter",
206
+ "UniversalErrorConverter",
207
+ "ErrorTranslator",
208
+ "setup_error_handling",
209
+ "create_litestar_exception_handlers",
210
+ "apply_litestar_openapi_problem_details",
211
+ "apply_api_errors",
212
+ "errors",
213
+ "raises_from",
214
+ "ErrorResponseParser",
215
+ "BackendError",
216
+ "ErrorAnalyzer",
217
+ "analyze_errors",
218
+ "openapi_errors",
219
+ "setup_automatic_error_docs",
220
+ "apply_auto_error_docs_to_router",
221
+ "auto_analyze_errors",
222
+ "WebSocketError",
223
+ "JSONRPCErrorCode",
224
+ "WebSocketAuthError",
225
+ "WebSocketTokenExpiredError",
226
+ "WebSocketRateLimitError",
227
+ "WebSocketValidationError",
228
+ "WebSocketMethodNotFoundError",
229
+ "WebSocketInternalError",
230
+ "WebSocketErrorHandler",
231
+ "setup_websocket_error_handling",
232
+ ]
@@ -0,0 +1,8 @@
1
+ from .error_analyzer import ErrorAnalyzer
2
+ from .decorators import analyze_errors, openapi_errors
3
+
4
+ __all__ = [
5
+ "ErrorAnalyzer",
6
+ "analyze_errors",
7
+ "openapi_errors",
8
+ ]
@@ -0,0 +1,251 @@
1
+ import functools
2
+ from typing import Any, Callable, Dict, List, Optional, Set, cast
3
+ from ..analysis.error_analyzer import ErrorAnalyzer
4
+ from ..core.error_codes import ErrorCode, ERROR_HTTP_STATUS_MAP
5
+
6
+
7
+ def analyze_errors(include_dependencies: bool = True):
8
+ """
9
+ Decorator to analyze all possible errors in a function.
10
+
11
+ Args:
12
+ include_dependencies: Whether to analyze called functions too
13
+
14
+ Returns:
15
+ Decorated function with error analysis attached
16
+ """
17
+
18
+ def decorator(func: Callable) -> Callable:
19
+ # Perform analysis
20
+ analyzer = ErrorAnalyzer(func)
21
+ analysis = analyzer.analyze()
22
+
23
+ # Attach analysis to function
24
+ setattr(func, "_error_analysis", analysis)
25
+
26
+ @functools.wraps(func)
27
+ def wrapper(*args, **kwargs):
28
+ return func(*args, **kwargs)
29
+
30
+ # Copy analysis to wrapper
31
+ setattr(wrapper, "_error_analysis", analysis)
32
+
33
+ return wrapper
34
+
35
+ return decorator
36
+
37
+
38
+ def openapi_errors(
39
+ additional_errors: Optional[List[str]] = None,
40
+ exclude_errors: Optional[List[str]] = None,
41
+ custom_descriptions: Optional[Dict[str, str]] = None,
42
+ ):
43
+ """
44
+ Decorator to generate OpenAPI error responses for a function.
45
+
46
+ Args:
47
+ additional_errors: Additional error codes to include
48
+ exclude_errors: Error codes to exclude from analysis
49
+ custom_descriptions: Custom descriptions for error codes
50
+
51
+ Returns:
52
+ Decorated function with OpenAPI responses attached
53
+ """
54
+
55
+ def decorator(func: Callable) -> Callable:
56
+ # Analyze function for errors
57
+ analyzer = ErrorAnalyzer(func)
58
+ analysis = analyzer.analyze()
59
+
60
+ # Get all error codes
61
+ error_codes = set(analysis["error_codes"])
62
+
63
+ # Add additional errors
64
+ if additional_errors:
65
+ error_codes.update(additional_errors)
66
+
67
+ # Remove excluded errors
68
+ if exclude_errors:
69
+ error_codes -= set(exclude_errors)
70
+
71
+ # Generate OpenAPI responses
72
+ openapi_responses = _generate_openapi_responses(
73
+ error_codes, custom_descriptions or {}
74
+ )
75
+
76
+ # Attach to function
77
+ setattr(func, "_openapi_error_responses", openapi_responses)
78
+ setattr(func, "_error_analysis", analysis)
79
+
80
+ @functools.wraps(func)
81
+ def wrapper(*args, **kwargs):
82
+ return func(*args, **kwargs)
83
+
84
+ # Copy to wrapper
85
+ setattr(wrapper, "_openapi_error_responses", openapi_responses)
86
+ setattr(wrapper, "_error_analysis", analysis)
87
+
88
+ return wrapper
89
+
90
+ return decorator
91
+
92
+
93
+ def _generate_openapi_responses(
94
+ error_codes: Set[str], custom_descriptions: Dict[str, str]
95
+ ) -> Dict[str, Dict[str, Any]]:
96
+ """Generate OpenAPI response schemas for error codes."""
97
+ responses = {}
98
+
99
+ # Group errors by HTTP status code
100
+ status_groups: Dict[int, List[str]] = {}
101
+
102
+ for error_code in error_codes:
103
+ try:
104
+ error_enum = ErrorCode(error_code)
105
+ status_code = ERROR_HTTP_STATUS_MAP.get(error_enum, 500)
106
+ except ValueError:
107
+ # Unknown error code
108
+ status_code = 500
109
+
110
+ if status_code not in status_groups:
111
+ status_groups[status_code] = []
112
+ status_groups[status_code].append(error_code)
113
+
114
+ # Generate response for each status code
115
+ for status_code, codes in status_groups.items():
116
+ response_schema = {
117
+ "description": _get_status_description(status_code, codes),
118
+ "content": {
119
+ "application/json": {
120
+ "schema": {
121
+ "type": "object",
122
+ "properties": {
123
+ "error": {
124
+ "type": "object",
125
+ "properties": {
126
+ "code": {
127
+ "type": "string",
128
+ "enum": codes,
129
+ "description": "Error code",
130
+ },
131
+ "message": {
132
+ "type": "string",
133
+ "description": "Error message",
134
+ },
135
+ "details": {
136
+ "type": "object",
137
+ "description": "Additional error details",
138
+ },
139
+ "timestamp": {
140
+ "type": "string",
141
+ "format": "date-time",
142
+ "description": "Error timestamp",
143
+ },
144
+ "request_id": {
145
+ "type": "string",
146
+ "description": "Request ID for tracing",
147
+ },
148
+ },
149
+ "required": [
150
+ "code",
151
+ "message",
152
+ "timestamp",
153
+ "request_id",
154
+ ],
155
+ }
156
+ },
157
+ "required": ["error"],
158
+ },
159
+ "examples": _generate_examples(codes, custom_descriptions),
160
+ }
161
+ },
162
+ }
163
+
164
+ responses[str(status_code)] = response_schema
165
+
166
+ return responses
167
+
168
+
169
+ def _get_status_description(status_code: int, error_codes: List[str]) -> str:
170
+ """Get description for HTTP status code."""
171
+ status_descriptions = {
172
+ 400: "Bad Request - Validation or input errors",
173
+ 401: "Unauthorized - Authentication required",
174
+ 403: "Forbidden - Insufficient permissions",
175
+ 404: "Not Found - Resource not found",
176
+ 409: "Conflict - Resource conflict (e.g., duplicate entry)",
177
+ 422: "Unprocessable Entity - Business logic errors",
178
+ 500: "Internal Server Error - Server errors",
179
+ }
180
+
181
+ base_desc = status_descriptions.get(status_code, f"HTTP {status_code}")
182
+ codes_str = ", ".join(error_codes)
183
+
184
+ return f"{base_desc}. Possible error codes: {codes_str}"
185
+
186
+
187
+ def _generate_examples(
188
+ error_codes: List[str], custom_descriptions: Dict[str, str]
189
+ ) -> Dict[str, Dict[str, Any]]:
190
+ """Generate example responses for error codes."""
191
+ examples = {}
192
+
193
+ for error_code in error_codes:
194
+ example_name = error_code.lower().replace("_", "-")
195
+ description = custom_descriptions.get(
196
+ error_code, _get_default_error_description(error_code)
197
+ )
198
+
199
+ examples[example_name] = {
200
+ "summary": f"{error_code} example",
201
+ "description": description,
202
+ "value": {
203
+ "error": {
204
+ "code": error_code,
205
+ "message": description,
206
+ "details": _get_example_details(error_code),
207
+ "timestamp": "2024-01-08T12:00:00Z",
208
+ "request_id": "req_abc123",
209
+ }
210
+ },
211
+ }
212
+
213
+ return examples
214
+
215
+
216
+ def _get_default_error_description(error_code: str) -> str:
217
+ """Get default description for error code."""
218
+ descriptions = {
219
+ "VALIDATION_FAILED": "Request validation failed",
220
+ "USER_NOT_FOUND": "User not found",
221
+ "AUTH_REQUIRED": "Authentication required",
222
+ "AUTH_PERMISSION_DENIED": "Permission denied",
223
+ "DB_DUPLICATE_ENTRY": "Duplicate entry in database",
224
+ "BUSINESS_RULE_VIOLATION": "Business rule violated",
225
+ }
226
+
227
+ return descriptions.get(error_code, f"Error: {error_code}")
228
+
229
+
230
+ def _get_example_details(error_code: str) -> Dict[str, Any]:
231
+ """Get example details for error code."""
232
+ example_details = {
233
+ "VALIDATION_FAILED": {
234
+ "field_errors": [
235
+ {
236
+ "field": "email",
237
+ "message": "invalid email format",
238
+ "input": "not-an-email",
239
+ }
240
+ ]
241
+ },
242
+ "USER_NOT_FOUND": {"resource": "user", "resource_id": 123},
243
+ "DB_DUPLICATE_ENTRY": {
244
+ "table": "users",
245
+ "field": "email",
246
+ "duplicate_value": "user@example.com",
247
+ },
248
+ "AUTH_PERMISSION_DENIED": {"required_permission": "admin.users.delete"},
249
+ }
250
+
251
+ return cast(Dict[str, Any], example_details.get(error_code, {}))