truthound-dashboard 1.0.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.
- truthound_dashboard/__init__.py +11 -0
- truthound_dashboard/__main__.py +6 -0
- truthound_dashboard/api/__init__.py +15 -0
- truthound_dashboard/api/deps.py +153 -0
- truthound_dashboard/api/drift.py +179 -0
- truthound_dashboard/api/error_handlers.py +287 -0
- truthound_dashboard/api/health.py +78 -0
- truthound_dashboard/api/history.py +62 -0
- truthound_dashboard/api/middleware.py +626 -0
- truthound_dashboard/api/notifications.py +561 -0
- truthound_dashboard/api/profile.py +52 -0
- truthound_dashboard/api/router.py +83 -0
- truthound_dashboard/api/rules.py +277 -0
- truthound_dashboard/api/schedules.py +329 -0
- truthound_dashboard/api/schemas.py +136 -0
- truthound_dashboard/api/sources.py +229 -0
- truthound_dashboard/api/validations.py +125 -0
- truthound_dashboard/cli.py +226 -0
- truthound_dashboard/config.py +132 -0
- truthound_dashboard/core/__init__.py +264 -0
- truthound_dashboard/core/base.py +185 -0
- truthound_dashboard/core/cache.py +479 -0
- truthound_dashboard/core/connections.py +331 -0
- truthound_dashboard/core/encryption.py +409 -0
- truthound_dashboard/core/exceptions.py +627 -0
- truthound_dashboard/core/logging.py +488 -0
- truthound_dashboard/core/maintenance.py +542 -0
- truthound_dashboard/core/notifications/__init__.py +56 -0
- truthound_dashboard/core/notifications/base.py +390 -0
- truthound_dashboard/core/notifications/channels.py +557 -0
- truthound_dashboard/core/notifications/dispatcher.py +453 -0
- truthound_dashboard/core/notifications/events.py +155 -0
- truthound_dashboard/core/notifications/service.py +744 -0
- truthound_dashboard/core/sampling.py +626 -0
- truthound_dashboard/core/scheduler.py +311 -0
- truthound_dashboard/core/services.py +1531 -0
- truthound_dashboard/core/truthound_adapter.py +659 -0
- truthound_dashboard/db/__init__.py +67 -0
- truthound_dashboard/db/base.py +108 -0
- truthound_dashboard/db/database.py +196 -0
- truthound_dashboard/db/models.py +732 -0
- truthound_dashboard/db/repository.py +237 -0
- truthound_dashboard/main.py +309 -0
- truthound_dashboard/schemas/__init__.py +150 -0
- truthound_dashboard/schemas/base.py +96 -0
- truthound_dashboard/schemas/drift.py +118 -0
- truthound_dashboard/schemas/history.py +74 -0
- truthound_dashboard/schemas/profile.py +91 -0
- truthound_dashboard/schemas/rule.py +199 -0
- truthound_dashboard/schemas/schedule.py +88 -0
- truthound_dashboard/schemas/schema.py +121 -0
- truthound_dashboard/schemas/source.py +138 -0
- truthound_dashboard/schemas/validation.py +192 -0
- truthound_dashboard/static/assets/index-BqJMyAHX.js +110 -0
- truthound_dashboard/static/assets/index-DMDxHCTs.js +465 -0
- truthound_dashboard/static/assets/index-Dm2D11TK.css +1 -0
- truthound_dashboard/static/index.html +15 -0
- truthound_dashboard/static/mockServiceWorker.js +349 -0
- truthound_dashboard-1.0.0.dist-info/METADATA +218 -0
- truthound_dashboard-1.0.0.dist-info/RECORD +62 -0
- truthound_dashboard-1.0.0.dist-info/WHEEL +4 -0
- truthound_dashboard-1.0.0.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
"""Hierarchical exception system with error codes and localization.
|
|
2
|
+
|
|
3
|
+
This module provides a comprehensive exception hierarchy for the dashboard,
|
|
4
|
+
supporting error codes, localized messages, and HTTP status code mapping.
|
|
5
|
+
|
|
6
|
+
The design follows these principles:
|
|
7
|
+
- Clear exception hierarchy for different error types
|
|
8
|
+
- Unique error codes for each exception type
|
|
9
|
+
- Support for localized error messages
|
|
10
|
+
- Easy HTTP status code mapping
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
try:
|
|
14
|
+
source = await get_source(source_id)
|
|
15
|
+
except SourceNotFoundError as e:
|
|
16
|
+
return JSONResponse(
|
|
17
|
+
status_code=e.http_status,
|
|
18
|
+
content=e.to_response(),
|
|
19
|
+
)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from enum import Enum
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ErrorCode(str, Enum):
|
|
29
|
+
"""Error codes for categorized exception types.
|
|
30
|
+
|
|
31
|
+
Format: {CATEGORY}_{SPECIFIC_ERROR}
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Generic errors (1xx)
|
|
35
|
+
UNKNOWN_ERROR = "E100"
|
|
36
|
+
INTERNAL_ERROR = "E101"
|
|
37
|
+
VALIDATION_ERROR = "E102"
|
|
38
|
+
|
|
39
|
+
# Source errors (2xx)
|
|
40
|
+
SOURCE_NOT_FOUND = "E200"
|
|
41
|
+
SOURCE_CONNECTION_FAILED = "E201"
|
|
42
|
+
SOURCE_INVALID_CONFIG = "E202"
|
|
43
|
+
SOURCE_ACCESS_DENIED = "E203"
|
|
44
|
+
|
|
45
|
+
# Schema errors (3xx)
|
|
46
|
+
SCHEMA_NOT_FOUND = "E300"
|
|
47
|
+
SCHEMA_INVALID = "E301"
|
|
48
|
+
SCHEMA_PARSE_ERROR = "E302"
|
|
49
|
+
|
|
50
|
+
# Rule errors (4xx)
|
|
51
|
+
RULE_NOT_FOUND = "E400"
|
|
52
|
+
RULE_INVALID = "E401"
|
|
53
|
+
RULE_PARSE_ERROR = "E402"
|
|
54
|
+
|
|
55
|
+
# Validation errors (5xx)
|
|
56
|
+
VALIDATION_NOT_FOUND = "E500"
|
|
57
|
+
VALIDATION_FAILED = "E501"
|
|
58
|
+
VALIDATION_TIMEOUT = "E502"
|
|
59
|
+
|
|
60
|
+
# Schedule errors (6xx)
|
|
61
|
+
SCHEDULE_NOT_FOUND = "E600"
|
|
62
|
+
SCHEDULE_INVALID_CRON = "E601"
|
|
63
|
+
SCHEDULE_CONFLICT = "E602"
|
|
64
|
+
|
|
65
|
+
# Notification errors (7xx)
|
|
66
|
+
NOTIFICATION_CHANNEL_NOT_FOUND = "E700"
|
|
67
|
+
NOTIFICATION_RULE_NOT_FOUND = "E701"
|
|
68
|
+
NOTIFICATION_SEND_FAILED = "E702"
|
|
69
|
+
NOTIFICATION_INVALID_CONFIG = "E703"
|
|
70
|
+
|
|
71
|
+
# Security errors (8xx)
|
|
72
|
+
AUTHENTICATION_REQUIRED = "E800"
|
|
73
|
+
AUTHENTICATION_FAILED = "E801"
|
|
74
|
+
AUTHORIZATION_FAILED = "E802"
|
|
75
|
+
RATE_LIMIT_EXCEEDED = "E803"
|
|
76
|
+
|
|
77
|
+
# Database errors (9xx)
|
|
78
|
+
DATABASE_ERROR = "E900"
|
|
79
|
+
DATABASE_CONNECTION_FAILED = "E901"
|
|
80
|
+
DATABASE_INTEGRITY_ERROR = "E902"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# Localized error messages
|
|
84
|
+
ERROR_MESSAGES: dict[str, dict[str, str]] = {
|
|
85
|
+
ErrorCode.UNKNOWN_ERROR: {
|
|
86
|
+
"en": "An unknown error occurred",
|
|
87
|
+
"ko": "알 수 없는 오류가 발생했습니다",
|
|
88
|
+
},
|
|
89
|
+
ErrorCode.INTERNAL_ERROR: {
|
|
90
|
+
"en": "An internal server error occurred. Please try again later.",
|
|
91
|
+
"ko": "내부 서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.",
|
|
92
|
+
},
|
|
93
|
+
ErrorCode.VALIDATION_ERROR: {
|
|
94
|
+
"en": "Validation failed. Please check your input.",
|
|
95
|
+
"ko": "검증에 실패했습니다. 입력을 확인해주세요.",
|
|
96
|
+
},
|
|
97
|
+
ErrorCode.SOURCE_NOT_FOUND: {
|
|
98
|
+
"en": "The data source could not be found. It may have been deleted.",
|
|
99
|
+
"ko": "데이터 소스를 찾을 수 없습니다. 삭제되었을 수 있습니다.",
|
|
100
|
+
},
|
|
101
|
+
ErrorCode.SOURCE_CONNECTION_FAILED: {
|
|
102
|
+
"en": "Could not connect to the data source. Please verify your connection settings.",
|
|
103
|
+
"ko": "데이터 소스에 연결할 수 없습니다. 연결 설정을 확인해주세요.",
|
|
104
|
+
},
|
|
105
|
+
ErrorCode.SOURCE_INVALID_CONFIG: {
|
|
106
|
+
"en": "Invalid source configuration. Please check the settings.",
|
|
107
|
+
"ko": "잘못된 소스 설정입니다. 설정을 확인해주세요.",
|
|
108
|
+
},
|
|
109
|
+
ErrorCode.SOURCE_ACCESS_DENIED: {
|
|
110
|
+
"en": "Access to the data source was denied.",
|
|
111
|
+
"ko": "데이터 소스에 대한 접근이 거부되었습니다.",
|
|
112
|
+
},
|
|
113
|
+
ErrorCode.SCHEMA_NOT_FOUND: {
|
|
114
|
+
"en": "Schema not found for this source.",
|
|
115
|
+
"ko": "이 소스에 대한 스키마를 찾을 수 없습니다.",
|
|
116
|
+
},
|
|
117
|
+
ErrorCode.SCHEMA_INVALID: {
|
|
118
|
+
"en": "The schema is invalid.",
|
|
119
|
+
"ko": "스키마가 유효하지 않습니다.",
|
|
120
|
+
},
|
|
121
|
+
ErrorCode.SCHEMA_PARSE_ERROR: {
|
|
122
|
+
"en": "Failed to parse schema. Check YAML syntax.",
|
|
123
|
+
"ko": "스키마 파싱에 실패했습니다. YAML 문법을 확인해주세요.",
|
|
124
|
+
},
|
|
125
|
+
ErrorCode.RULE_NOT_FOUND: {
|
|
126
|
+
"en": "Rule not found.",
|
|
127
|
+
"ko": "규칙을 찾을 수 없습니다.",
|
|
128
|
+
},
|
|
129
|
+
ErrorCode.RULE_INVALID: {
|
|
130
|
+
"en": "The rule configuration is invalid.",
|
|
131
|
+
"ko": "규칙 설정이 유효하지 않습니다.",
|
|
132
|
+
},
|
|
133
|
+
ErrorCode.RULE_PARSE_ERROR: {
|
|
134
|
+
"en": "Failed to parse rule. Check YAML syntax.",
|
|
135
|
+
"ko": "규칙 파싱에 실패했습니다. YAML 문법을 확인해주세요.",
|
|
136
|
+
},
|
|
137
|
+
ErrorCode.VALIDATION_NOT_FOUND: {
|
|
138
|
+
"en": "Validation result not found.",
|
|
139
|
+
"ko": "검증 결과를 찾을 수 없습니다.",
|
|
140
|
+
},
|
|
141
|
+
ErrorCode.VALIDATION_FAILED: {
|
|
142
|
+
"en": "Validation failed. Please check your data and rules.",
|
|
143
|
+
"ko": "검증에 실패했습니다. 데이터와 규칙을 확인해주세요.",
|
|
144
|
+
},
|
|
145
|
+
ErrorCode.VALIDATION_TIMEOUT: {
|
|
146
|
+
"en": "Validation timed out. Try with a smaller dataset or increase timeout.",
|
|
147
|
+
"ko": "검증 시간이 초과되었습니다. 더 작은 데이터셋을 사용하거나 타임아웃을 늘려주세요.",
|
|
148
|
+
},
|
|
149
|
+
ErrorCode.SCHEDULE_NOT_FOUND: {
|
|
150
|
+
"en": "Schedule not found.",
|
|
151
|
+
"ko": "스케줄을 찾을 수 없습니다.",
|
|
152
|
+
},
|
|
153
|
+
ErrorCode.SCHEDULE_INVALID_CRON: {
|
|
154
|
+
"en": "Invalid cron expression. Please check the schedule syntax.",
|
|
155
|
+
"ko": "잘못된 cron 표현식입니다. 스케줄 문법을 확인해주세요.",
|
|
156
|
+
},
|
|
157
|
+
ErrorCode.SCHEDULE_CONFLICT: {
|
|
158
|
+
"en": "Schedule conflict detected.",
|
|
159
|
+
"ko": "스케줄 충돌이 감지되었습니다.",
|
|
160
|
+
},
|
|
161
|
+
ErrorCode.NOTIFICATION_CHANNEL_NOT_FOUND: {
|
|
162
|
+
"en": "Notification channel not found.",
|
|
163
|
+
"ko": "알림 채널을 찾을 수 없습니다.",
|
|
164
|
+
},
|
|
165
|
+
ErrorCode.NOTIFICATION_RULE_NOT_FOUND: {
|
|
166
|
+
"en": "Notification rule not found.",
|
|
167
|
+
"ko": "알림 규칙을 찾을 수 없습니다.",
|
|
168
|
+
},
|
|
169
|
+
ErrorCode.NOTIFICATION_SEND_FAILED: {
|
|
170
|
+
"en": "Failed to send notification. Please check your channel configuration.",
|
|
171
|
+
"ko": "알림 발송에 실패했습니다. 채널 설정을 확인해주세요.",
|
|
172
|
+
},
|
|
173
|
+
ErrorCode.NOTIFICATION_INVALID_CONFIG: {
|
|
174
|
+
"en": "Invalid notification configuration.",
|
|
175
|
+
"ko": "잘못된 알림 설정입니다.",
|
|
176
|
+
},
|
|
177
|
+
ErrorCode.AUTHENTICATION_REQUIRED: {
|
|
178
|
+
"en": "Authentication is required.",
|
|
179
|
+
"ko": "인증이 필요합니다.",
|
|
180
|
+
},
|
|
181
|
+
ErrorCode.AUTHENTICATION_FAILED: {
|
|
182
|
+
"en": "Authentication failed. Please check your credentials.",
|
|
183
|
+
"ko": "인증에 실패했습니다. 자격 증명을 확인해주세요.",
|
|
184
|
+
},
|
|
185
|
+
ErrorCode.AUTHORIZATION_FAILED: {
|
|
186
|
+
"en": "You do not have permission to perform this action.",
|
|
187
|
+
"ko": "이 작업을 수행할 권한이 없습니다.",
|
|
188
|
+
},
|
|
189
|
+
ErrorCode.RATE_LIMIT_EXCEEDED: {
|
|
190
|
+
"en": "Too many requests. Please try again later.",
|
|
191
|
+
"ko": "요청이 너무 많습니다. 잠시 후 다시 시도해주세요.",
|
|
192
|
+
},
|
|
193
|
+
ErrorCode.DATABASE_ERROR: {
|
|
194
|
+
"en": "A database error occurred.",
|
|
195
|
+
"ko": "데이터베이스 오류가 발생했습니다.",
|
|
196
|
+
},
|
|
197
|
+
ErrorCode.DATABASE_CONNECTION_FAILED: {
|
|
198
|
+
"en": "Failed to connect to the database.",
|
|
199
|
+
"ko": "데이터베이스 연결에 실패했습니다.",
|
|
200
|
+
},
|
|
201
|
+
ErrorCode.DATABASE_INTEGRITY_ERROR: {
|
|
202
|
+
"en": "Database integrity error. Data may be corrupted.",
|
|
203
|
+
"ko": "데이터베이스 무결성 오류입니다. 데이터가 손상되었을 수 있습니다.",
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def get_error_message(code: ErrorCode | str, lang: str = "en") -> str:
|
|
209
|
+
"""Get localized error message for an error code.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
code: Error code.
|
|
213
|
+
lang: Language code ('en' or 'ko').
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Localized error message.
|
|
217
|
+
"""
|
|
218
|
+
if isinstance(code, str):
|
|
219
|
+
code = ErrorCode(code)
|
|
220
|
+
messages = ERROR_MESSAGES.get(code, ERROR_MESSAGES[ErrorCode.UNKNOWN_ERROR])
|
|
221
|
+
return messages.get(lang, messages.get("en", "An error occurred"))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class TruthoundDashboardError(Exception):
|
|
225
|
+
"""Base exception for truthound-dashboard.
|
|
226
|
+
|
|
227
|
+
All custom exceptions should inherit from this class.
|
|
228
|
+
|
|
229
|
+
Attributes:
|
|
230
|
+
message: Human-readable error message.
|
|
231
|
+
code: Error code enum.
|
|
232
|
+
details: Additional error details.
|
|
233
|
+
http_status: HTTP status code for API responses.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
code: ErrorCode = ErrorCode.UNKNOWN_ERROR
|
|
237
|
+
http_status: int = 500
|
|
238
|
+
|
|
239
|
+
def __init__(
|
|
240
|
+
self,
|
|
241
|
+
message: str | None = None,
|
|
242
|
+
details: dict[str, Any] | None = None,
|
|
243
|
+
lang: str = "en",
|
|
244
|
+
) -> None:
|
|
245
|
+
"""Initialize exception.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
message: Custom error message. Uses default if not provided.
|
|
249
|
+
details: Additional error details.
|
|
250
|
+
lang: Language for default message.
|
|
251
|
+
"""
|
|
252
|
+
self.message = message or get_error_message(self.code, lang)
|
|
253
|
+
self.details = details or {}
|
|
254
|
+
self.lang = lang
|
|
255
|
+
super().__init__(self.message)
|
|
256
|
+
|
|
257
|
+
def to_response(self) -> dict[str, Any]:
|
|
258
|
+
"""Convert exception to API response format.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Dictionary suitable for JSON response.
|
|
262
|
+
"""
|
|
263
|
+
response = {
|
|
264
|
+
"success": False,
|
|
265
|
+
"error": {
|
|
266
|
+
"code": self.code.value,
|
|
267
|
+
"message": self.message,
|
|
268
|
+
},
|
|
269
|
+
}
|
|
270
|
+
if self.details:
|
|
271
|
+
response["error"]["details"] = self.details
|
|
272
|
+
return response
|
|
273
|
+
|
|
274
|
+
def __repr__(self) -> str:
|
|
275
|
+
return f"{self.__class__.__name__}(code={self.code.value}, message={self.message!r})"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# =============================================================================
|
|
279
|
+
# Source Errors
|
|
280
|
+
# =============================================================================
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class SourceError(TruthoundDashboardError):
|
|
284
|
+
"""Base class for source-related errors."""
|
|
285
|
+
|
|
286
|
+
code = ErrorCode.SOURCE_NOT_FOUND
|
|
287
|
+
http_status = 404
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class SourceNotFoundError(SourceError):
|
|
291
|
+
"""Raised when a data source cannot be found."""
|
|
292
|
+
|
|
293
|
+
code = ErrorCode.SOURCE_NOT_FOUND
|
|
294
|
+
http_status = 404
|
|
295
|
+
|
|
296
|
+
def __init__(self, source_id: str, **kwargs: Any) -> None:
|
|
297
|
+
"""Initialize with source ID.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
source_id: ID of the source that was not found.
|
|
301
|
+
**kwargs: Additional arguments for parent class.
|
|
302
|
+
"""
|
|
303
|
+
super().__init__(details={"source_id": source_id}, **kwargs)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class SourceConnectionError(SourceError):
|
|
307
|
+
"""Raised when connection to a data source fails."""
|
|
308
|
+
|
|
309
|
+
code = ErrorCode.SOURCE_CONNECTION_FAILED
|
|
310
|
+
http_status = 502
|
|
311
|
+
|
|
312
|
+
def __init__(
|
|
313
|
+
self,
|
|
314
|
+
source_type: str,
|
|
315
|
+
reason: str | None = None,
|
|
316
|
+
**kwargs: Any,
|
|
317
|
+
) -> None:
|
|
318
|
+
"""Initialize with source type and reason.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
source_type: Type of source (file, postgresql, etc.).
|
|
322
|
+
reason: Reason for connection failure.
|
|
323
|
+
**kwargs: Additional arguments for parent class.
|
|
324
|
+
"""
|
|
325
|
+
details = {"source_type": source_type}
|
|
326
|
+
if reason:
|
|
327
|
+
details["reason"] = reason
|
|
328
|
+
super().__init__(details=details, **kwargs)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class SourceInvalidConfigError(SourceError):
|
|
332
|
+
"""Raised when source configuration is invalid."""
|
|
333
|
+
|
|
334
|
+
code = ErrorCode.SOURCE_INVALID_CONFIG
|
|
335
|
+
http_status = 400
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class SourceAccessDeniedError(SourceError):
|
|
339
|
+
"""Raised when access to a source is denied."""
|
|
340
|
+
|
|
341
|
+
code = ErrorCode.SOURCE_ACCESS_DENIED
|
|
342
|
+
http_status = 403
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# =============================================================================
|
|
346
|
+
# Schema Errors
|
|
347
|
+
# =============================================================================
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class SchemaError(TruthoundDashboardError):
|
|
351
|
+
"""Base class for schema-related errors."""
|
|
352
|
+
|
|
353
|
+
code = ErrorCode.SCHEMA_NOT_FOUND
|
|
354
|
+
http_status = 404
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class SchemaNotFoundError(SchemaError):
|
|
358
|
+
"""Raised when a schema cannot be found."""
|
|
359
|
+
|
|
360
|
+
code = ErrorCode.SCHEMA_NOT_FOUND
|
|
361
|
+
http_status = 404
|
|
362
|
+
|
|
363
|
+
def __init__(self, source_id: str, **kwargs: Any) -> None:
|
|
364
|
+
super().__init__(details={"source_id": source_id}, **kwargs)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class SchemaInvalidError(SchemaError):
|
|
368
|
+
"""Raised when a schema is invalid."""
|
|
369
|
+
|
|
370
|
+
code = ErrorCode.SCHEMA_INVALID
|
|
371
|
+
http_status = 400
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class SchemaParseError(SchemaError):
|
|
375
|
+
"""Raised when schema parsing fails."""
|
|
376
|
+
|
|
377
|
+
code = ErrorCode.SCHEMA_PARSE_ERROR
|
|
378
|
+
http_status = 400
|
|
379
|
+
|
|
380
|
+
def __init__(self, parse_error: str, **kwargs: Any) -> None:
|
|
381
|
+
super().__init__(details={"parse_error": parse_error}, **kwargs)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# =============================================================================
|
|
385
|
+
# Rule Errors
|
|
386
|
+
# =============================================================================
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class RuleError(TruthoundDashboardError):
|
|
390
|
+
"""Base class for rule-related errors."""
|
|
391
|
+
|
|
392
|
+
code = ErrorCode.RULE_NOT_FOUND
|
|
393
|
+
http_status = 404
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class RuleNotFoundError(RuleError):
|
|
397
|
+
"""Raised when a rule cannot be found."""
|
|
398
|
+
|
|
399
|
+
code = ErrorCode.RULE_NOT_FOUND
|
|
400
|
+
http_status = 404
|
|
401
|
+
|
|
402
|
+
def __init__(self, rule_id: str, **kwargs: Any) -> None:
|
|
403
|
+
super().__init__(details={"rule_id": rule_id}, **kwargs)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class RuleInvalidError(RuleError):
|
|
407
|
+
"""Raised when a rule is invalid."""
|
|
408
|
+
|
|
409
|
+
code = ErrorCode.RULE_INVALID
|
|
410
|
+
http_status = 400
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class RuleParseError(RuleError):
|
|
414
|
+
"""Raised when rule parsing fails."""
|
|
415
|
+
|
|
416
|
+
code = ErrorCode.RULE_PARSE_ERROR
|
|
417
|
+
http_status = 400
|
|
418
|
+
|
|
419
|
+
def __init__(self, parse_error: str, **kwargs: Any) -> None:
|
|
420
|
+
super().__init__(details={"parse_error": parse_error}, **kwargs)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
# =============================================================================
|
|
424
|
+
# Validation Errors
|
|
425
|
+
# =============================================================================
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class ValidationError(TruthoundDashboardError):
|
|
429
|
+
"""Base class for validation-related errors."""
|
|
430
|
+
|
|
431
|
+
code = ErrorCode.VALIDATION_ERROR
|
|
432
|
+
http_status = 400
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
class ValidationNotFoundError(ValidationError):
|
|
436
|
+
"""Raised when a validation result cannot be found."""
|
|
437
|
+
|
|
438
|
+
code = ErrorCode.VALIDATION_NOT_FOUND
|
|
439
|
+
http_status = 404
|
|
440
|
+
|
|
441
|
+
def __init__(self, validation_id: str, **kwargs: Any) -> None:
|
|
442
|
+
super().__init__(details={"validation_id": validation_id}, **kwargs)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class ValidationFailedError(ValidationError):
|
|
446
|
+
"""Raised when validation execution fails."""
|
|
447
|
+
|
|
448
|
+
code = ErrorCode.VALIDATION_FAILED
|
|
449
|
+
http_status = 500
|
|
450
|
+
|
|
451
|
+
def __init__(self, reason: str, **kwargs: Any) -> None:
|
|
452
|
+
super().__init__(details={"reason": reason}, **kwargs)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class ValidationTimeoutError(ValidationError):
|
|
456
|
+
"""Raised when validation times out."""
|
|
457
|
+
|
|
458
|
+
code = ErrorCode.VALIDATION_TIMEOUT
|
|
459
|
+
http_status = 504
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
# =============================================================================
|
|
463
|
+
# Schedule Errors
|
|
464
|
+
# =============================================================================
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
class ScheduleError(TruthoundDashboardError):
|
|
468
|
+
"""Base class for schedule-related errors."""
|
|
469
|
+
|
|
470
|
+
code = ErrorCode.SCHEDULE_NOT_FOUND
|
|
471
|
+
http_status = 404
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class ScheduleNotFoundError(ScheduleError):
|
|
475
|
+
"""Raised when a schedule cannot be found."""
|
|
476
|
+
|
|
477
|
+
code = ErrorCode.SCHEDULE_NOT_FOUND
|
|
478
|
+
http_status = 404
|
|
479
|
+
|
|
480
|
+
def __init__(self, schedule_id: str, **kwargs: Any) -> None:
|
|
481
|
+
super().__init__(details={"schedule_id": schedule_id}, **kwargs)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
class ScheduleInvalidCronError(ScheduleError):
|
|
485
|
+
"""Raised when a cron expression is invalid."""
|
|
486
|
+
|
|
487
|
+
code = ErrorCode.SCHEDULE_INVALID_CRON
|
|
488
|
+
http_status = 400
|
|
489
|
+
|
|
490
|
+
def __init__(self, cron_expression: str, **kwargs: Any) -> None:
|
|
491
|
+
super().__init__(details={"cron_expression": cron_expression}, **kwargs)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
class ScheduleConflictError(ScheduleError):
|
|
495
|
+
"""Raised when there is a schedule conflict."""
|
|
496
|
+
|
|
497
|
+
code = ErrorCode.SCHEDULE_CONFLICT
|
|
498
|
+
http_status = 409
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
# =============================================================================
|
|
502
|
+
# Notification Errors
|
|
503
|
+
# =============================================================================
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
class NotificationError(TruthoundDashboardError):
|
|
507
|
+
"""Base class for notification-related errors."""
|
|
508
|
+
|
|
509
|
+
code = ErrorCode.NOTIFICATION_SEND_FAILED
|
|
510
|
+
http_status = 500
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
class NotificationChannelNotFoundError(NotificationError):
|
|
514
|
+
"""Raised when a notification channel cannot be found."""
|
|
515
|
+
|
|
516
|
+
code = ErrorCode.NOTIFICATION_CHANNEL_NOT_FOUND
|
|
517
|
+
http_status = 404
|
|
518
|
+
|
|
519
|
+
def __init__(self, channel_id: str, **kwargs: Any) -> None:
|
|
520
|
+
super().__init__(details={"channel_id": channel_id}, **kwargs)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
class NotificationRuleNotFoundError(NotificationError):
|
|
524
|
+
"""Raised when a notification rule cannot be found."""
|
|
525
|
+
|
|
526
|
+
code = ErrorCode.NOTIFICATION_RULE_NOT_FOUND
|
|
527
|
+
http_status = 404
|
|
528
|
+
|
|
529
|
+
def __init__(self, rule_id: str, **kwargs: Any) -> None:
|
|
530
|
+
super().__init__(details={"rule_id": rule_id}, **kwargs)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
class NotificationSendError(NotificationError):
|
|
534
|
+
"""Raised when notification delivery fails."""
|
|
535
|
+
|
|
536
|
+
code = ErrorCode.NOTIFICATION_SEND_FAILED
|
|
537
|
+
http_status = 502
|
|
538
|
+
|
|
539
|
+
def __init__(
|
|
540
|
+
self,
|
|
541
|
+
channel_type: str,
|
|
542
|
+
reason: str | None = None,
|
|
543
|
+
**kwargs: Any,
|
|
544
|
+
) -> None:
|
|
545
|
+
details = {"channel_type": channel_type}
|
|
546
|
+
if reason:
|
|
547
|
+
details["reason"] = reason
|
|
548
|
+
super().__init__(details=details, **kwargs)
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
class NotificationInvalidConfigError(NotificationError):
|
|
552
|
+
"""Raised when notification configuration is invalid."""
|
|
553
|
+
|
|
554
|
+
code = ErrorCode.NOTIFICATION_INVALID_CONFIG
|
|
555
|
+
http_status = 400
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
# =============================================================================
|
|
559
|
+
# Security Errors
|
|
560
|
+
# =============================================================================
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
class SecurityError(TruthoundDashboardError):
|
|
564
|
+
"""Base class for security-related errors."""
|
|
565
|
+
|
|
566
|
+
code = ErrorCode.AUTHENTICATION_REQUIRED
|
|
567
|
+
http_status = 401
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
class AuthenticationRequiredError(SecurityError):
|
|
571
|
+
"""Raised when authentication is required but not provided."""
|
|
572
|
+
|
|
573
|
+
code = ErrorCode.AUTHENTICATION_REQUIRED
|
|
574
|
+
http_status = 401
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
class AuthenticationFailedError(SecurityError):
|
|
578
|
+
"""Raised when authentication fails."""
|
|
579
|
+
|
|
580
|
+
code = ErrorCode.AUTHENTICATION_FAILED
|
|
581
|
+
http_status = 401
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
class AuthorizationError(SecurityError):
|
|
585
|
+
"""Raised when authorization fails."""
|
|
586
|
+
|
|
587
|
+
code = ErrorCode.AUTHORIZATION_FAILED
|
|
588
|
+
http_status = 403
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
class RateLimitExceededError(SecurityError):
|
|
592
|
+
"""Raised when rate limit is exceeded."""
|
|
593
|
+
|
|
594
|
+
code = ErrorCode.RATE_LIMIT_EXCEEDED
|
|
595
|
+
http_status = 429
|
|
596
|
+
|
|
597
|
+
def __init__(self, retry_after: int | None = None, **kwargs: Any) -> None:
|
|
598
|
+
details = {}
|
|
599
|
+
if retry_after:
|
|
600
|
+
details["retry_after"] = retry_after
|
|
601
|
+
super().__init__(details=details, **kwargs)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
# =============================================================================
|
|
605
|
+
# Database Errors
|
|
606
|
+
# =============================================================================
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
class DatabaseError(TruthoundDashboardError):
|
|
610
|
+
"""Base class for database-related errors."""
|
|
611
|
+
|
|
612
|
+
code = ErrorCode.DATABASE_ERROR
|
|
613
|
+
http_status = 500
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
class DatabaseConnectionError(DatabaseError):
|
|
617
|
+
"""Raised when database connection fails."""
|
|
618
|
+
|
|
619
|
+
code = ErrorCode.DATABASE_CONNECTION_FAILED
|
|
620
|
+
http_status = 503
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
class DatabaseIntegrityError(DatabaseError):
|
|
624
|
+
"""Raised when database integrity is violated."""
|
|
625
|
+
|
|
626
|
+
code = ErrorCode.DATABASE_INTEGRITY_ERROR
|
|
627
|
+
http_status = 500
|