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,123 @@
1
+ from typing import Any, Optional
2
+
3
+ from ..core.error_codes import ErrorCode
4
+ from ..core.exceptions import AppError
5
+ from .generic import generic_error_handler
6
+ from .python_converter import PythonErrorConverter
7
+ from .sql_converter import SQLErrorConverter
8
+
9
+ SQLAlchemyErrorType: type[Exception] | None = None
10
+ try: # pragma: no cover - optional dependency
11
+ from sqlalchemy.exc import SQLAlchemyError as _LoadedSQLAlchemyError
12
+ except ImportError: # pragma: no cover
13
+ pass
14
+ else: # pragma: no cover
15
+ SQLAlchemyErrorType = _LoadedSQLAlchemyError
16
+
17
+ PydanticValidationErrorType: type[Exception] | None = None
18
+ try: # pragma: no cover - optional dependency
19
+ from pydantic import ValidationError as _LoadedPydanticValidationError
20
+ except ImportError: # pragma: no cover
21
+ pass
22
+ else: # pragma: no cover
23
+ PydanticValidationErrorType = _LoadedPydanticValidationError
24
+
25
+ _PydanticErrorConverter: Any = None
26
+ try: # pragma: no cover - optional dependency
27
+ from .pydantic_converter import PydanticErrorConverter as _LoadedPydanticErrorConverter
28
+ except ImportError: # pragma: no cover
29
+ pass
30
+ else: # pragma: no cover
31
+ _PydanticErrorConverter = _LoadedPydanticErrorConverter
32
+
33
+
34
+ class UniversalErrorConverter:
35
+ """Universal error converter that handles any type of exception."""
36
+
37
+ @classmethod
38
+ def convert(cls, error: Exception, debug: bool = False) -> AppError:
39
+ """
40
+ Convert any exception to AppError.
41
+
42
+ Args:
43
+ error: Any exception
44
+ debug: Include debug information
45
+
46
+ Returns:
47
+ AppError instance with appropriate details
48
+ """
49
+ # Check if it's already an AppError
50
+ if isinstance(error, AppError):
51
+ return error
52
+
53
+ # Pydantic validation errors
54
+ if PydanticValidationErrorType is not None and isinstance(
55
+ error, PydanticValidationErrorType
56
+ ):
57
+ if _PydanticErrorConverter is None:
58
+ raise ImportError(
59
+ "Install 'awesome-errors[pydantic]' to convert Pydantic validation errors."
60
+ ) from None
61
+ return _PydanticErrorConverter.convert(error)
62
+
63
+ # SQLAlchemy errors
64
+ if SQLAlchemyErrorType is not None and isinstance(error, SQLAlchemyErrorType):
65
+ return SQLErrorConverter.convert(error)
66
+
67
+ # Standard Python exceptions
68
+ if type(error) in PythonErrorConverter.EXCEPTION_MAP:
69
+ return PythonErrorConverter.convert(error)
70
+
71
+ # Handle other specific error types
72
+ app_error = cls._handle_special_cases(error)
73
+ if app_error:
74
+ return app_error
75
+
76
+ # Default handling for unknown errors
77
+ return generic_error_handler(error, debug)
78
+
79
+ @classmethod
80
+ def _handle_special_cases(cls, error: Exception) -> Optional[AppError]:
81
+ """Handle special error cases."""
82
+ error_type = type(error).__name__
83
+ error_str = str(error)
84
+
85
+ # Handle HTTP-related errors
86
+ if "HTTPError" in error_type:
87
+ return AppError(
88
+ code=ErrorCode.INTERNAL_ERROR,
89
+ message="HTTP request failed",
90
+ details={"error_type": error_type, "error": error_str},
91
+ )
92
+
93
+ # Handle JSON errors
94
+ if "JSONDecodeError" in error_type:
95
+ return AppError(
96
+ code=ErrorCode.INVALID_FORMAT,
97
+ message="Invalid JSON format",
98
+ details={"error_type": error_type, "error": error_str},
99
+ )
100
+
101
+ # Handle import errors
102
+ if isinstance(error, (ImportError, ModuleNotFoundError)):
103
+ return AppError(
104
+ code=ErrorCode.INTERNAL_ERROR,
105
+ message="Missing required module",
106
+ details={
107
+ "module": error.name if hasattr(error, "name") else "unknown",
108
+ "error": error_str,
109
+ },
110
+ )
111
+
112
+ return None
113
+
114
+ @staticmethod
115
+ def _is_serializable(value: Any) -> bool:
116
+ """Check if value can be safely serialized."""
117
+ try:
118
+ import json
119
+
120
+ json.dumps(value)
121
+ return True
122
+ except Exception:
123
+ return False
File without changes
@@ -0,0 +1,104 @@
1
+ from enum import StrEnum
2
+ from typing import Dict
3
+
4
+
5
+ class ErrorCode(StrEnum):
6
+ # General errors
7
+ UNKNOWN_ERROR = "UNKNOWN_ERROR"
8
+ INTERNAL_ERROR = "INTERNAL_ERROR"
9
+
10
+ # Validation errors (400)
11
+ VALIDATION_FAILED = "VALIDATION_FAILED"
12
+ INVALID_INPUT = "INVALID_INPUT"
13
+ MISSING_REQUIRED_FIELD = "MISSING_REQUIRED_FIELD"
14
+ INVALID_FORMAT = "INVALID_FORMAT"
15
+
16
+ # Authentication errors (401)
17
+ AUTH_REQUIRED = "AUTH_REQUIRED"
18
+ AUTH_INVALID_TOKEN = "AUTH_INVALID_TOKEN"
19
+ AUTH_TOKEN_EXPIRED = "AUTH_TOKEN_EXPIRED"
20
+ REFRESH_TOKEN_REUSE = "REFRESH_TOKEN_REUSE"
21
+ SESSION_EXPIRED = "SESSION_EXPIRED"
22
+
23
+ # Authorization errors (403)
24
+ AUTH_PERMISSION_DENIED = "AUTH_PERMISSION_DENIED"
25
+ AUTH_INSUFFICIENT_PRIVILEGES = "AUTH_INSUFFICIENT_PRIVILEGES"
26
+
27
+ # Not found errors (404)
28
+ RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND"
29
+ RESOURCE_ALREADY_EXISTS = "RESOURCE_ALREADY_EXISTS"
30
+ USER_NOT_FOUND = "USER_NOT_FOUND"
31
+ ENTITY_NOT_FOUND = "ENTITY_NOT_FOUND"
32
+ OAUTH_PROVIDER_UNKNOWN = "OAUTH_PROVIDER_UNKNOWN"
33
+
34
+ # Database errors (500)
35
+ DB_CONNECTION_ERROR = "DB_CONNECTION_ERROR"
36
+ DB_QUERY_ERROR = "DB_QUERY_ERROR"
37
+ DB_TRANSACTION_ERROR = "DB_TRANSACTION_ERROR"
38
+ DB_CONSTRAINT_VIOLATION = "DB_CONSTRAINT_VIOLATION"
39
+ DB_DUPLICATE_ENTRY = "DB_DUPLICATE_ENTRY"
40
+ DB_INVALID_REFERENCE = "DB_INVALID_REFERENCE"
41
+ DB_MISSING_REQUIRED = "DB_MISSING_REQUIRED"
42
+
43
+ # Business logic errors
44
+ BUSINESS_RULE_VIOLATION = "BUSINESS_RULE_VIOLATION"
45
+ INSUFFICIENT_BALANCE = "INSUFFICIENT_BALANCE"
46
+ OPERATION_NOT_ALLOWED = "OPERATION_NOT_ALLOWED"
47
+ RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"
48
+ SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE"
49
+
50
+ @classmethod
51
+ def _missing_(cls, value):
52
+ """Create new ErrorCode for unknown values."""
53
+ if isinstance(value, str):
54
+ # Create a pseudo member for custom values
55
+ pseudo_member = str.__new__(cls, value)
56
+ pseudo_member._name_ = value
57
+ pseudo_member._value_ = value
58
+ return pseudo_member
59
+ return None
60
+
61
+
62
+ ERROR_HTTP_STATUS_MAP: Dict[ErrorCode, int] = {
63
+ # 400 Bad Request
64
+ ErrorCode.VALIDATION_FAILED: 400,
65
+ ErrorCode.INVALID_INPUT: 400,
66
+ ErrorCode.MISSING_REQUIRED_FIELD: 400,
67
+ ErrorCode.INVALID_FORMAT: 400,
68
+ # 401 Unauthorized
69
+ ErrorCode.AUTH_REQUIRED: 401,
70
+ ErrorCode.AUTH_INVALID_TOKEN: 401,
71
+ ErrorCode.AUTH_TOKEN_EXPIRED: 401,
72
+ ErrorCode.REFRESH_TOKEN_REUSE: 401,
73
+ ErrorCode.SESSION_EXPIRED: 401,
74
+ # 403 Forbidden
75
+ ErrorCode.AUTH_PERMISSION_DENIED: 403,
76
+ ErrorCode.AUTH_INSUFFICIENT_PRIVILEGES: 403,
77
+ # 404 Not Found
78
+ ErrorCode.RESOURCE_NOT_FOUND: 404,
79
+ ErrorCode.RESOURCE_ALREADY_EXISTS: 409,
80
+ ErrorCode.USER_NOT_FOUND: 404,
81
+ ErrorCode.ENTITY_NOT_FOUND: 404,
82
+ ErrorCode.OAUTH_PROVIDER_UNKNOWN: 404,
83
+ # 422 Unprocessable Entity
84
+ ErrorCode.BUSINESS_RULE_VIOLATION: 422,
85
+ ErrorCode.INSUFFICIENT_BALANCE: 422,
86
+ ErrorCode.OPERATION_NOT_ALLOWED: 422,
87
+ ErrorCode.RATE_LIMIT_EXCEEDED: 429,
88
+ # 500 Internal Server Error
89
+ ErrorCode.UNKNOWN_ERROR: 500,
90
+ ErrorCode.INTERNAL_ERROR: 500,
91
+ ErrorCode.SERVICE_UNAVAILABLE: 503,
92
+ ErrorCode.DB_CONNECTION_ERROR: 500,
93
+ ErrorCode.DB_QUERY_ERROR: 500,
94
+ ErrorCode.DB_TRANSACTION_ERROR: 500,
95
+ ErrorCode.DB_CONSTRAINT_VIOLATION: 409, # Conflict
96
+ ErrorCode.DB_DUPLICATE_ENTRY: 409,
97
+ ErrorCode.DB_INVALID_REFERENCE: 422,
98
+ ErrorCode.DB_MISSING_REQUIRED: 422,
99
+ }
100
+
101
+
102
+ def get_http_status(error_code: ErrorCode) -> int:
103
+ """Get HTTP status code for an error code."""
104
+ return ERROR_HTTP_STATUS_MAP.get(error_code, 500)
@@ -0,0 +1,61 @@
1
+ """Core error response models built on msgspec."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from typing import Any, Dict, Mapping
7
+
8
+ import msgspec
9
+
10
+
11
+ def _now_utc() -> datetime:
12
+ return datetime.now(timezone.utc)
13
+
14
+
15
+ class ErrorDetail(msgspec.Struct, kw_only=True, omit_defaults=True):
16
+ """Serializable representation of an application error."""
17
+
18
+ code: str
19
+ message: str
20
+ request_id: str
21
+ details: Dict[str, Any] = msgspec.field(default_factory=dict)
22
+ timestamp: datetime = msgspec.field(default_factory=_now_utc)
23
+
24
+ def to_dict(self) -> Dict[str, Any]:
25
+ """Convert to builtin types, ensuring ISO timestamps."""
26
+ data = msgspec.to_builtins(self, builtin_types=None)
27
+ timestamp = data.get("timestamp")
28
+ if isinstance(timestamp, datetime):
29
+ data["timestamp"] = timestamp.isoformat().replace("+00:00", "Z")
30
+ elif isinstance(timestamp, str):
31
+ data["timestamp"] = timestamp
32
+ else:
33
+ data["timestamp"] = _now_utc().isoformat().replace("+00:00", "Z")
34
+ return data
35
+
36
+
37
+ class ErrorResponse(msgspec.Struct, kw_only=True, omit_defaults=True):
38
+ """Legacy `{"error": {...}}` envelope."""
39
+
40
+ error: ErrorDetail
41
+
42
+ def to_dict(self) -> Dict[str, Any]:
43
+ return {"error": self.error.to_dict()}
44
+
45
+
46
+ def error_detail_from_mapping(data: Mapping[str, Any]) -> ErrorDetail:
47
+ """Construct an ``ErrorDetail`` from mapping data."""
48
+ timestamp = data.get("timestamp")
49
+ if isinstance(timestamp, str):
50
+ try:
51
+ timestamp = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
52
+ except ValueError:
53
+ timestamp = _now_utc()
54
+
55
+ return ErrorDetail(
56
+ code=str(data.get("code", "UNKNOWN_ERROR")),
57
+ message=str(data.get("message", "")),
58
+ details=dict(data.get("details") or {}),
59
+ timestamp=timestamp if isinstance(timestamp, datetime) else _now_utc(),
60
+ request_id=str(data.get("request_id", "")),
61
+ )