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,613 @@
|
|
|
1
|
+
from typing import Any, ClassVar, Dict, Optional, Union, TYPE_CHECKING
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from .error_codes import ErrorCode, get_http_status
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from litestar.openapi.datastructures import ResponseSpec
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AppError(Exception):
|
|
12
|
+
"""
|
|
13
|
+
Base application error class for server-side error handling.
|
|
14
|
+
|
|
15
|
+
This is the core exception class used throughout the application.
|
|
16
|
+
It provides standardized error structure with:
|
|
17
|
+
- Error code (from ErrorCode enum or custom string)
|
|
18
|
+
- Human-readable message
|
|
19
|
+
- Additional details dictionary
|
|
20
|
+
- Timestamp and request ID for tracing
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
raise AppError(ErrorCode.USER_NOT_FOUND, "User not found", {"user_id": 123})
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
code: ErrorCode
|
|
27
|
+
message: str
|
|
28
|
+
details: Dict[str, Any]
|
|
29
|
+
timestamp: datetime
|
|
30
|
+
request_id: str | None
|
|
31
|
+
status_code: int
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
code: Union[ErrorCode, str],
|
|
36
|
+
message: str,
|
|
37
|
+
details: Optional[Dict[str, Any]] = None,
|
|
38
|
+
status_code: Optional[int] = None,
|
|
39
|
+
):
|
|
40
|
+
super().__init__(message)
|
|
41
|
+
self.code = code if isinstance(code, ErrorCode) else ErrorCode(code)
|
|
42
|
+
self.message = message
|
|
43
|
+
self.details = details or {}
|
|
44
|
+
self.timestamp = datetime.now(timezone.utc)
|
|
45
|
+
self.request_id = str(uuid.uuid4())
|
|
46
|
+
|
|
47
|
+
# Use provided status code or get from mapping
|
|
48
|
+
self.status_code = status_code or get_http_status(self.code)
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
51
|
+
"""Convert error to dictionary for JSON serialization."""
|
|
52
|
+
return {
|
|
53
|
+
"error": {
|
|
54
|
+
"code": self.code.value,
|
|
55
|
+
"message": self.message,
|
|
56
|
+
"details": self.details,
|
|
57
|
+
"timestamp": self.timestamp.isoformat() + "Z",
|
|
58
|
+
"request_id": self.request_id,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class APIError(AppError):
|
|
64
|
+
"""Base HTTP-facing error with OpenAPI metadata."""
|
|
65
|
+
|
|
66
|
+
error_code: ClassVar[Union[ErrorCode, str]] = ErrorCode.UNKNOWN_ERROR
|
|
67
|
+
http_status_code: ClassVar[int | None] = None
|
|
68
|
+
title: ClassVar[str] = "Internal error"
|
|
69
|
+
description: ClassVar[str] = "An internal error occurred"
|
|
70
|
+
response_media_type: ClassVar[str] = "application/problem+json"
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
code: Union[ErrorCode, str] | None = None,
|
|
75
|
+
message: Optional[str] = None,
|
|
76
|
+
details: Optional[Dict[str, Any]] = None,
|
|
77
|
+
status_code: Optional[int] = None,
|
|
78
|
+
):
|
|
79
|
+
effective_code = code or self.error_code
|
|
80
|
+
if status_code is not None:
|
|
81
|
+
effective_status = status_code
|
|
82
|
+
elif code is not None:
|
|
83
|
+
normalized = (
|
|
84
|
+
effective_code
|
|
85
|
+
if isinstance(effective_code, ErrorCode)
|
|
86
|
+
else ErrorCode(effective_code)
|
|
87
|
+
)
|
|
88
|
+
effective_status = get_http_status(normalized)
|
|
89
|
+
else:
|
|
90
|
+
normalized = (
|
|
91
|
+
effective_code
|
|
92
|
+
if isinstance(effective_code, ErrorCode)
|
|
93
|
+
else ErrorCode(effective_code)
|
|
94
|
+
)
|
|
95
|
+
effective_status = (
|
|
96
|
+
self.http_status_code
|
|
97
|
+
if self.http_status_code is not None
|
|
98
|
+
else get_http_status(normalized)
|
|
99
|
+
)
|
|
100
|
+
super().__init__(
|
|
101
|
+
effective_code,
|
|
102
|
+
message or self.title,
|
|
103
|
+
details,
|
|
104
|
+
effective_status,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def get_status_code(cls) -> int:
|
|
109
|
+
if cls.http_status_code is not None:
|
|
110
|
+
return cls.http_status_code
|
|
111
|
+
error_code = (
|
|
112
|
+
cls.error_code if isinstance(cls.error_code, ErrorCode) else ErrorCode(cls.error_code)
|
|
113
|
+
)
|
|
114
|
+
return get_http_status(error_code)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def openapi_response(cls) -> "ResponseSpec": # type: ignore[name-defined]
|
|
118
|
+
try:
|
|
119
|
+
from litestar.openapi.datastructures import ResponseSpec # type: ignore
|
|
120
|
+
except ImportError as exc: # pragma: no cover - optional dependency
|
|
121
|
+
msg = "Litestar is required to build OpenAPI response specs"
|
|
122
|
+
raise RuntimeError(msg) from exc
|
|
123
|
+
|
|
124
|
+
return ResponseSpec(
|
|
125
|
+
data_container=None,
|
|
126
|
+
description=cls.description,
|
|
127
|
+
media_type=cls.response_media_type,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ValidationError(APIError):
|
|
132
|
+
"""
|
|
133
|
+
Validation error (HTTP 400) for input validation failures.
|
|
134
|
+
|
|
135
|
+
Used when user input fails validation rules.
|
|
136
|
+
Can include field-specific information.
|
|
137
|
+
|
|
138
|
+
Usage:
|
|
139
|
+
raise ValidationError("Email is required", field="email")
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.VALIDATION_FAILED
|
|
143
|
+
http_status_code: ClassVar[int] = 400
|
|
144
|
+
title: ClassVar[str] = "Request validation failed"
|
|
145
|
+
description: ClassVar[str] = "Request validation failed"
|
|
146
|
+
|
|
147
|
+
def __init__(
|
|
148
|
+
self,
|
|
149
|
+
message: Optional[str] = None,
|
|
150
|
+
field: Optional[str] = None,
|
|
151
|
+
value: Any = None,
|
|
152
|
+
code: ErrorCode = ErrorCode.VALIDATION_FAILED,
|
|
153
|
+
):
|
|
154
|
+
details: Dict[str, Any] = {}
|
|
155
|
+
if field:
|
|
156
|
+
details["field"] = field
|
|
157
|
+
if value is not None:
|
|
158
|
+
details["value"] = value
|
|
159
|
+
|
|
160
|
+
super().__init__(code, message, details, 400)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class InvalidInputError(ValidationError):
|
|
164
|
+
"""Invalid input error (HTTP 400)."""
|
|
165
|
+
|
|
166
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.INVALID_INPUT
|
|
167
|
+
title: ClassVar[str] = "Invalid input"
|
|
168
|
+
description: ClassVar[str] = "Invalid input"
|
|
169
|
+
|
|
170
|
+
def __init__(self, message: Optional[str] = None, field: Optional[str] = None):
|
|
171
|
+
super().__init__(message or self.title, field=field, code=self.error_code)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class MissingRequiredFieldError(ValidationError):
|
|
175
|
+
"""Missing required field error (HTTP 400)."""
|
|
176
|
+
|
|
177
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.MISSING_REQUIRED_FIELD
|
|
178
|
+
title: ClassVar[str] = "Missing required field"
|
|
179
|
+
description: ClassVar[str] = "Missing required field"
|
|
180
|
+
|
|
181
|
+
def __init__(self, message: Optional[str] = None, field: Optional[str] = None):
|
|
182
|
+
super().__init__(message or self.title, field=field, code=self.error_code)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class InvalidFormatError(ValidationError):
|
|
186
|
+
"""Invalid format error (HTTP 400)."""
|
|
187
|
+
|
|
188
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.INVALID_FORMAT
|
|
189
|
+
title: ClassVar[str] = "Invalid format"
|
|
190
|
+
description: ClassVar[str] = "Invalid format"
|
|
191
|
+
|
|
192
|
+
def __init__(self, message: Optional[str] = None, field: Optional[str] = None):
|
|
193
|
+
super().__init__(message or self.title, field=field, code=self.error_code)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class AuthError(APIError):
|
|
197
|
+
"""
|
|
198
|
+
Authentication/Authorization error (HTTP 401/403).
|
|
199
|
+
|
|
200
|
+
Used for authentication and authorization failures.
|
|
201
|
+
Can include required permission information.
|
|
202
|
+
|
|
203
|
+
Usage:
|
|
204
|
+
raise AuthError("Admin access required", required_permission="admin.users.read")
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.AUTH_REQUIRED
|
|
208
|
+
http_status_code: ClassVar[int] = 401
|
|
209
|
+
title: ClassVar[str] = "Authentication required"
|
|
210
|
+
description: ClassVar[str] = "Authentication required"
|
|
211
|
+
|
|
212
|
+
def __init__(
|
|
213
|
+
self,
|
|
214
|
+
message: Optional[str] = None,
|
|
215
|
+
code: ErrorCode | None = None,
|
|
216
|
+
required_permission: Optional[str] = None,
|
|
217
|
+
status_code: Optional[int] = None,
|
|
218
|
+
):
|
|
219
|
+
details: Dict[str, Any] = {}
|
|
220
|
+
if required_permission:
|
|
221
|
+
details["required_permission"] = required_permission
|
|
222
|
+
|
|
223
|
+
super().__init__(code, message, details, status_code)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class AuthRequiredError(AuthError):
|
|
227
|
+
"""Authentication required error (HTTP 401)."""
|
|
228
|
+
|
|
229
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.AUTH_REQUIRED
|
|
230
|
+
title: ClassVar[str] = "Authentication required"
|
|
231
|
+
description: ClassVar[str] = "Authentication required"
|
|
232
|
+
|
|
233
|
+
def __init__(
|
|
234
|
+
self, message: Optional[str] = None, required_permission: Optional[str] = None
|
|
235
|
+
):
|
|
236
|
+
super().__init__(
|
|
237
|
+
message,
|
|
238
|
+
code=self.error_code,
|
|
239
|
+
required_permission=required_permission,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class AuthInvalidTokenError(AuthError):
|
|
244
|
+
"""Invalid token error (HTTP 401)."""
|
|
245
|
+
|
|
246
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.AUTH_INVALID_TOKEN
|
|
247
|
+
title: ClassVar[str] = "Invalid token"
|
|
248
|
+
description: ClassVar[str] = "Invalid token"
|
|
249
|
+
|
|
250
|
+
def __init__(self, message: Optional[str] = None):
|
|
251
|
+
super().__init__(message, code=self.error_code)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class AuthTokenExpiredError(AuthError):
|
|
255
|
+
"""Token expired error (HTTP 401)."""
|
|
256
|
+
|
|
257
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.AUTH_TOKEN_EXPIRED
|
|
258
|
+
title: ClassVar[str] = "Token expired"
|
|
259
|
+
description: ClassVar[str] = "Token expired"
|
|
260
|
+
|
|
261
|
+
def __init__(self, message: Optional[str] = None):
|
|
262
|
+
super().__init__(message, code=self.error_code)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class AuthPermissionDeniedError(AuthError):
|
|
266
|
+
"""Permission denied error (HTTP 403)."""
|
|
267
|
+
|
|
268
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.AUTH_PERMISSION_DENIED
|
|
269
|
+
http_status_code: ClassVar[int] = 403
|
|
270
|
+
title: ClassVar[str] = "Access denied"
|
|
271
|
+
description: ClassVar[str] = "Access denied"
|
|
272
|
+
|
|
273
|
+
def __init__(
|
|
274
|
+
self, message: Optional[str] = None, required_permission: Optional[str] = None
|
|
275
|
+
):
|
|
276
|
+
super().__init__(
|
|
277
|
+
message,
|
|
278
|
+
code=self.error_code,
|
|
279
|
+
required_permission=required_permission,
|
|
280
|
+
status_code=self.http_status_code,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class AuthInsufficientPrivilegesError(AuthError):
|
|
285
|
+
"""Insufficient privileges error (HTTP 403)."""
|
|
286
|
+
|
|
287
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.AUTH_INSUFFICIENT_PRIVILEGES
|
|
288
|
+
http_status_code: ClassVar[int] = 403
|
|
289
|
+
title: ClassVar[str] = "Insufficient privileges"
|
|
290
|
+
description: ClassVar[str] = "Insufficient privileges"
|
|
291
|
+
|
|
292
|
+
def __init__(
|
|
293
|
+
self, message: Optional[str] = None, required_permission: Optional[str] = None
|
|
294
|
+
):
|
|
295
|
+
super().__init__(
|
|
296
|
+
message,
|
|
297
|
+
code=self.error_code,
|
|
298
|
+
required_permission=required_permission,
|
|
299
|
+
status_code=self.http_status_code,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class SessionExpiredError(AuthError):
|
|
304
|
+
"""Session expired error (HTTP 401)."""
|
|
305
|
+
|
|
306
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.SESSION_EXPIRED
|
|
307
|
+
title: ClassVar[str] = "Session has expired"
|
|
308
|
+
description: ClassVar[str] = "Session has expired"
|
|
309
|
+
|
|
310
|
+
def __init__(self, message: Optional[str] = None):
|
|
311
|
+
super().__init__(message or self.title, code=self.error_code)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class RefreshTokenReuseDetectedError(AuthError):
|
|
315
|
+
"""Refresh token reuse detected (HTTP 401)."""
|
|
316
|
+
|
|
317
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.REFRESH_TOKEN_REUSE
|
|
318
|
+
title: ClassVar[str] = "Refresh token reuse detected"
|
|
319
|
+
description: ClassVar[str] = "Refresh token reuse detected"
|
|
320
|
+
|
|
321
|
+
def __init__(self, message: Optional[str] = None):
|
|
322
|
+
super().__init__(message or self.title, code=self.error_code)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class NotFoundError(APIError):
|
|
326
|
+
"""
|
|
327
|
+
Resource not found error (HTTP 404).
|
|
328
|
+
|
|
329
|
+
Used when a requested resource doesn't exist.
|
|
330
|
+
Includes resource type and ID information.
|
|
331
|
+
|
|
332
|
+
Usage:
|
|
333
|
+
raise NotFoundError("user", user_id=123)
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.RESOURCE_NOT_FOUND
|
|
337
|
+
http_status_code: ClassVar[int] = 404
|
|
338
|
+
title: ClassVar[str] = "Resource not found"
|
|
339
|
+
description: ClassVar[str] = "Resource not found"
|
|
340
|
+
|
|
341
|
+
def __init__(
|
|
342
|
+
self,
|
|
343
|
+
resource: str,
|
|
344
|
+
resource_id: Optional[Union[str, int]] = None,
|
|
345
|
+
code: ErrorCode = ErrorCode.RESOURCE_NOT_FOUND,
|
|
346
|
+
):
|
|
347
|
+
details: Dict[str, Any] = {"resource": resource}
|
|
348
|
+
if resource_id is not None:
|
|
349
|
+
details["resource_id"] = resource_id
|
|
350
|
+
|
|
351
|
+
message = f"{resource} not found"
|
|
352
|
+
if resource_id:
|
|
353
|
+
message += f" with id: {resource_id}"
|
|
354
|
+
|
|
355
|
+
super().__init__(code, message, details, 404)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class ResourceNotFoundError(NotFoundError):
|
|
359
|
+
"""Resource not found error (HTTP 404)."""
|
|
360
|
+
|
|
361
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.RESOURCE_NOT_FOUND
|
|
362
|
+
title: ClassVar[str] = "Resource not found"
|
|
363
|
+
description: ClassVar[str] = "Resource not found"
|
|
364
|
+
|
|
365
|
+
def __init__(self, resource: str, resource_id: Optional[Union[str, int]] = None):
|
|
366
|
+
super().__init__(resource, resource_id=resource_id, code=self.error_code)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class UserNotFoundError(NotFoundError):
|
|
370
|
+
"""User not found error (HTTP 404)."""
|
|
371
|
+
|
|
372
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.USER_NOT_FOUND
|
|
373
|
+
title: ClassVar[str] = "User not found"
|
|
374
|
+
description: ClassVar[str] = "User not found"
|
|
375
|
+
|
|
376
|
+
def __init__(self, user_id: Optional[Union[str, int]] = None):
|
|
377
|
+
super().__init__("user", resource_id=user_id, code=self.error_code)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class EntityNotFoundError(NotFoundError):
|
|
381
|
+
"""Entity not found error (HTTP 404)."""
|
|
382
|
+
|
|
383
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.ENTITY_NOT_FOUND
|
|
384
|
+
title: ClassVar[str] = "Entity not found"
|
|
385
|
+
description: ClassVar[str] = "Entity not found"
|
|
386
|
+
|
|
387
|
+
def __init__(self, entity: str, entity_id: Optional[Union[str, int]] = None):
|
|
388
|
+
super().__init__(entity, resource_id=entity_id, code=self.error_code)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class OAuthProviderUnknownError(NotFoundError):
|
|
392
|
+
"""OAuth provider not found error (HTTP 404)."""
|
|
393
|
+
|
|
394
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.OAUTH_PROVIDER_UNKNOWN
|
|
395
|
+
title: ClassVar[str] = "OAuth provider not found"
|
|
396
|
+
description: ClassVar[str] = "OAuth provider not found"
|
|
397
|
+
|
|
398
|
+
def __init__(self, provider: str):
|
|
399
|
+
super().__init__("oauth_provider", resource_id=provider, code=self.error_code)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class DatabaseError(APIError):
|
|
403
|
+
"""
|
|
404
|
+
Database error (HTTP 500/409/422).
|
|
405
|
+
|
|
406
|
+
Used for database-related errors like constraint violations,
|
|
407
|
+
connection issues, or query failures.
|
|
408
|
+
|
|
409
|
+
Usage:
|
|
410
|
+
raise DatabaseError("Duplicate email", table="users", sql_error="...")
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.DB_QUERY_ERROR
|
|
414
|
+
title: ClassVar[str] = "Database query error"
|
|
415
|
+
description: ClassVar[str] = "Database query error"
|
|
416
|
+
|
|
417
|
+
def __init__(
|
|
418
|
+
self,
|
|
419
|
+
message: Optional[str] = None,
|
|
420
|
+
code: ErrorCode = ErrorCode.DB_QUERY_ERROR,
|
|
421
|
+
sql_error: Optional[str] = None,
|
|
422
|
+
table: Optional[str] = None,
|
|
423
|
+
):
|
|
424
|
+
details: Dict[str, Any] = {}
|
|
425
|
+
if sql_error:
|
|
426
|
+
details["sql_error"] = sql_error
|
|
427
|
+
if table:
|
|
428
|
+
details["table"] = table
|
|
429
|
+
|
|
430
|
+
super().__init__(code, message, details)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class DatabaseConnectionError(DatabaseError):
|
|
434
|
+
"""Database connection error (HTTP 500)."""
|
|
435
|
+
|
|
436
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.DB_CONNECTION_ERROR
|
|
437
|
+
title: ClassVar[str] = "Database connection error"
|
|
438
|
+
description: ClassVar[str] = "Database connection error"
|
|
439
|
+
|
|
440
|
+
def __init__(self, message: Optional[str] = None, sql_error: Optional[str] = None):
|
|
441
|
+
super().__init__(message or self.title, code=self.error_code, sql_error=sql_error)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
class DatabaseQueryError(DatabaseError):
|
|
445
|
+
"""Database query error (HTTP 500)."""
|
|
446
|
+
|
|
447
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.DB_QUERY_ERROR
|
|
448
|
+
title: ClassVar[str] = "Database query error"
|
|
449
|
+
description: ClassVar[str] = "Database query error"
|
|
450
|
+
|
|
451
|
+
def __init__(self, message: Optional[str] = None, sql_error: Optional[str] = None):
|
|
452
|
+
super().__init__(message or self.title, code=self.error_code, sql_error=sql_error)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class DatabaseTransactionError(DatabaseError):
|
|
456
|
+
"""Database transaction error (HTTP 500)."""
|
|
457
|
+
|
|
458
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.DB_TRANSACTION_ERROR
|
|
459
|
+
title: ClassVar[str] = "Database transaction error"
|
|
460
|
+
description: ClassVar[str] = "Database transaction error"
|
|
461
|
+
|
|
462
|
+
def __init__(self, message: Optional[str] = None, sql_error: Optional[str] = None):
|
|
463
|
+
super().__init__(message or self.title, code=self.error_code, sql_error=sql_error)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class DatabaseConstraintViolationError(DatabaseError):
|
|
467
|
+
"""Database constraint violation (HTTP 409)."""
|
|
468
|
+
|
|
469
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.DB_CONSTRAINT_VIOLATION
|
|
470
|
+
title: ClassVar[str] = "Database constraint violation"
|
|
471
|
+
description: ClassVar[str] = "Database constraint violation"
|
|
472
|
+
|
|
473
|
+
def __init__(
|
|
474
|
+
self,
|
|
475
|
+
message: Optional[str] = None,
|
|
476
|
+
sql_error: Optional[str] = None,
|
|
477
|
+
table: Optional[str] = None,
|
|
478
|
+
):
|
|
479
|
+
super().__init__(
|
|
480
|
+
message or self.title,
|
|
481
|
+
code=self.error_code,
|
|
482
|
+
sql_error=sql_error,
|
|
483
|
+
table=table,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
class DatabaseDuplicateEntryError(DatabaseError):
|
|
488
|
+
"""Database duplicate entry (HTTP 409)."""
|
|
489
|
+
|
|
490
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.DB_DUPLICATE_ENTRY
|
|
491
|
+
title: ClassVar[str] = "Database duplicate entry"
|
|
492
|
+
description: ClassVar[str] = "Database duplicate entry"
|
|
493
|
+
|
|
494
|
+
def __init__(
|
|
495
|
+
self,
|
|
496
|
+
message: Optional[str] = None,
|
|
497
|
+
sql_error: Optional[str] = None,
|
|
498
|
+
table: Optional[str] = None,
|
|
499
|
+
):
|
|
500
|
+
super().__init__(
|
|
501
|
+
message or self.title,
|
|
502
|
+
code=self.error_code,
|
|
503
|
+
sql_error=sql_error,
|
|
504
|
+
table=table,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class DatabaseInvalidReferenceError(DatabaseError):
|
|
509
|
+
"""Database invalid reference (HTTP 422)."""
|
|
510
|
+
|
|
511
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.DB_INVALID_REFERENCE
|
|
512
|
+
title: ClassVar[str] = "Database invalid reference"
|
|
513
|
+
description: ClassVar[str] = "Database invalid reference"
|
|
514
|
+
|
|
515
|
+
def __init__(
|
|
516
|
+
self,
|
|
517
|
+
message: Optional[str] = None,
|
|
518
|
+
sql_error: Optional[str] = None,
|
|
519
|
+
table: Optional[str] = None,
|
|
520
|
+
):
|
|
521
|
+
super().__init__(
|
|
522
|
+
message or self.title,
|
|
523
|
+
code=self.error_code,
|
|
524
|
+
sql_error=sql_error,
|
|
525
|
+
table=table,
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class DatabaseMissingRequiredError(DatabaseError):
|
|
530
|
+
"""Database missing required field (HTTP 422)."""
|
|
531
|
+
|
|
532
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.DB_MISSING_REQUIRED
|
|
533
|
+
title: ClassVar[str] = "Database missing required field"
|
|
534
|
+
description: ClassVar[str] = "Database missing required field"
|
|
535
|
+
|
|
536
|
+
def __init__(
|
|
537
|
+
self,
|
|
538
|
+
message: Optional[str] = None,
|
|
539
|
+
sql_error: Optional[str] = None,
|
|
540
|
+
table: Optional[str] = None,
|
|
541
|
+
):
|
|
542
|
+
super().__init__(
|
|
543
|
+
message or self.title,
|
|
544
|
+
code=self.error_code,
|
|
545
|
+
sql_error=sql_error,
|
|
546
|
+
table=table,
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
class BusinessLogicError(APIError):
|
|
551
|
+
"""
|
|
552
|
+
Business logic error (HTTP 422).
|
|
553
|
+
|
|
554
|
+
Used for business rule violations and domain-specific errors.
|
|
555
|
+
Can include rule name and context information.
|
|
556
|
+
|
|
557
|
+
Usage:
|
|
558
|
+
raise BusinessLogicError("Insufficient balance", rule="min_balance", context={"current": 50})
|
|
559
|
+
"""
|
|
560
|
+
|
|
561
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.BUSINESS_RULE_VIOLATION
|
|
562
|
+
http_status_code: ClassVar[int] = 422
|
|
563
|
+
title: ClassVar[str] = "Business rule violation"
|
|
564
|
+
description: ClassVar[str] = "Business rule violation"
|
|
565
|
+
|
|
566
|
+
def __init__(
|
|
567
|
+
self,
|
|
568
|
+
message: Optional[str] = None,
|
|
569
|
+
code: ErrorCode = ErrorCode.BUSINESS_RULE_VIOLATION,
|
|
570
|
+
rule: Optional[str] = None,
|
|
571
|
+
context: Optional[Dict[str, Any]] = None,
|
|
572
|
+
):
|
|
573
|
+
details: Dict[str, Any] = {}
|
|
574
|
+
if rule:
|
|
575
|
+
details["rule"] = rule
|
|
576
|
+
if context:
|
|
577
|
+
details["context"] = context
|
|
578
|
+
|
|
579
|
+
super().__init__(code, message, details, 422)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
class InsufficientBalanceError(BusinessLogicError):
|
|
583
|
+
"""Insufficient balance error (HTTP 422)."""
|
|
584
|
+
|
|
585
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.INSUFFICIENT_BALANCE
|
|
586
|
+
title: ClassVar[str] = "Insufficient balance"
|
|
587
|
+
description: ClassVar[str] = "Insufficient balance"
|
|
588
|
+
|
|
589
|
+
def __init__(
|
|
590
|
+
self, message: Optional[str] = None, context: Optional[Dict[str, Any]] = None
|
|
591
|
+
):
|
|
592
|
+
super().__init__(
|
|
593
|
+
message or self.title,
|
|
594
|
+
code=self.error_code,
|
|
595
|
+
context=context,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
class OperationNotAllowedError(BusinessLogicError):
|
|
600
|
+
"""Operation not allowed error (HTTP 422)."""
|
|
601
|
+
|
|
602
|
+
error_code: ClassVar[ErrorCode] = ErrorCode.OPERATION_NOT_ALLOWED
|
|
603
|
+
title: ClassVar[str] = "Operation not allowed"
|
|
604
|
+
description: ClassVar[str] = "Operation not allowed"
|
|
605
|
+
|
|
606
|
+
def __init__(
|
|
607
|
+
self, message: Optional[str] = None, context: Optional[Dict[str, Any]] = None
|
|
608
|
+
):
|
|
609
|
+
super().__init__(
|
|
610
|
+
message or self.title,
|
|
611
|
+
code=self.error_code,
|
|
612
|
+
context=context,
|
|
613
|
+
)
|