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.
Files changed (62) hide show
  1. truthound_dashboard/__init__.py +11 -0
  2. truthound_dashboard/__main__.py +6 -0
  3. truthound_dashboard/api/__init__.py +15 -0
  4. truthound_dashboard/api/deps.py +153 -0
  5. truthound_dashboard/api/drift.py +179 -0
  6. truthound_dashboard/api/error_handlers.py +287 -0
  7. truthound_dashboard/api/health.py +78 -0
  8. truthound_dashboard/api/history.py +62 -0
  9. truthound_dashboard/api/middleware.py +626 -0
  10. truthound_dashboard/api/notifications.py +561 -0
  11. truthound_dashboard/api/profile.py +52 -0
  12. truthound_dashboard/api/router.py +83 -0
  13. truthound_dashboard/api/rules.py +277 -0
  14. truthound_dashboard/api/schedules.py +329 -0
  15. truthound_dashboard/api/schemas.py +136 -0
  16. truthound_dashboard/api/sources.py +229 -0
  17. truthound_dashboard/api/validations.py +125 -0
  18. truthound_dashboard/cli.py +226 -0
  19. truthound_dashboard/config.py +132 -0
  20. truthound_dashboard/core/__init__.py +264 -0
  21. truthound_dashboard/core/base.py +185 -0
  22. truthound_dashboard/core/cache.py +479 -0
  23. truthound_dashboard/core/connections.py +331 -0
  24. truthound_dashboard/core/encryption.py +409 -0
  25. truthound_dashboard/core/exceptions.py +627 -0
  26. truthound_dashboard/core/logging.py +488 -0
  27. truthound_dashboard/core/maintenance.py +542 -0
  28. truthound_dashboard/core/notifications/__init__.py +56 -0
  29. truthound_dashboard/core/notifications/base.py +390 -0
  30. truthound_dashboard/core/notifications/channels.py +557 -0
  31. truthound_dashboard/core/notifications/dispatcher.py +453 -0
  32. truthound_dashboard/core/notifications/events.py +155 -0
  33. truthound_dashboard/core/notifications/service.py +744 -0
  34. truthound_dashboard/core/sampling.py +626 -0
  35. truthound_dashboard/core/scheduler.py +311 -0
  36. truthound_dashboard/core/services.py +1531 -0
  37. truthound_dashboard/core/truthound_adapter.py +659 -0
  38. truthound_dashboard/db/__init__.py +67 -0
  39. truthound_dashboard/db/base.py +108 -0
  40. truthound_dashboard/db/database.py +196 -0
  41. truthound_dashboard/db/models.py +732 -0
  42. truthound_dashboard/db/repository.py +237 -0
  43. truthound_dashboard/main.py +309 -0
  44. truthound_dashboard/schemas/__init__.py +150 -0
  45. truthound_dashboard/schemas/base.py +96 -0
  46. truthound_dashboard/schemas/drift.py +118 -0
  47. truthound_dashboard/schemas/history.py +74 -0
  48. truthound_dashboard/schemas/profile.py +91 -0
  49. truthound_dashboard/schemas/rule.py +199 -0
  50. truthound_dashboard/schemas/schedule.py +88 -0
  51. truthound_dashboard/schemas/schema.py +121 -0
  52. truthound_dashboard/schemas/source.py +138 -0
  53. truthound_dashboard/schemas/validation.py +192 -0
  54. truthound_dashboard/static/assets/index-BqJMyAHX.js +110 -0
  55. truthound_dashboard/static/assets/index-DMDxHCTs.js +465 -0
  56. truthound_dashboard/static/assets/index-Dm2D11TK.css +1 -0
  57. truthound_dashboard/static/index.html +15 -0
  58. truthound_dashboard/static/mockServiceWorker.js +349 -0
  59. truthound_dashboard-1.0.0.dist-info/METADATA +218 -0
  60. truthound_dashboard-1.0.0.dist-info/RECORD +62 -0
  61. truthound_dashboard-1.0.0.dist-info/WHEEL +4 -0
  62. 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