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,390 @@
1
+ """Base classes for the notification system.
2
+
3
+ This module provides abstract base classes and protocols for implementing
4
+ notification channels with a pluggable architecture.
5
+
6
+ Design Patterns:
7
+ - Strategy Pattern: Each channel type implements a common interface
8
+ - Registry Pattern: Channels are registered and discovered dynamically
9
+ - Template Method: Common notification flow with customizable steps
10
+
11
+ Example:
12
+ @ChannelRegistry.register("custom")
13
+ class CustomChannel(BaseNotificationChannel):
14
+ @classmethod
15
+ def get_config_schema(cls) -> dict:
16
+ return {"url": {"type": "string", "required": True}}
17
+
18
+ async def send(self, message: str, **kwargs) -> bool:
19
+ # Custom implementation
20
+ return True
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from abc import ABC, abstractmethod
26
+ from dataclasses import dataclass, field
27
+ from datetime import datetime
28
+ from enum import Enum
29
+ from typing import Any, ClassVar
30
+
31
+
32
+ class NotificationStatus(str, Enum):
33
+ """Status of a notification delivery attempt."""
34
+
35
+ PENDING = "pending"
36
+ SENT = "sent"
37
+ FAILED = "failed"
38
+
39
+
40
+ @dataclass
41
+ class NotificationResult:
42
+ """Result of a notification delivery attempt.
43
+
44
+ Attributes:
45
+ success: Whether the notification was delivered successfully.
46
+ channel_id: ID of the channel used.
47
+ channel_type: Type of the channel (slack, email, etc.).
48
+ message: The message that was sent.
49
+ error: Error message if delivery failed.
50
+ sent_at: Timestamp of the delivery attempt.
51
+ metadata: Additional metadata about the delivery.
52
+ """
53
+
54
+ success: bool
55
+ channel_id: str
56
+ channel_type: str
57
+ message: str
58
+ error: str | None = None
59
+ sent_at: datetime = field(default_factory=datetime.utcnow)
60
+ metadata: dict[str, Any] = field(default_factory=dict)
61
+
62
+
63
+ @dataclass
64
+ class NotificationEvent:
65
+ """Base class for notification events.
66
+
67
+ Events represent triggering conditions for notifications.
68
+ Subclasses define specific event types with their data.
69
+
70
+ Attributes:
71
+ event_type: Type identifier for the event.
72
+ source_id: Optional source ID related to the event.
73
+ source_name: Optional source name for display.
74
+ timestamp: When the event occurred.
75
+ data: Additional event-specific data.
76
+ """
77
+
78
+ event_type: str
79
+ source_id: str | None = None
80
+ source_name: str | None = None
81
+ timestamp: datetime = field(default_factory=datetime.utcnow)
82
+ data: dict[str, Any] = field(default_factory=dict)
83
+
84
+ def to_dict(self) -> dict[str, Any]:
85
+ """Convert event to dictionary for serialization."""
86
+ return {
87
+ "event_type": self.event_type,
88
+ "source_id": self.source_id,
89
+ "source_name": self.source_name,
90
+ "timestamp": self.timestamp.isoformat(),
91
+ "data": self.data,
92
+ }
93
+
94
+
95
+ class BaseNotificationChannel(ABC):
96
+ """Abstract base class for notification channels.
97
+
98
+ Each channel type (Slack, Email, Webhook, etc.) implements this
99
+ interface to provide consistent notification delivery.
100
+
101
+ Class Attributes:
102
+ channel_type: Unique identifier for this channel type.
103
+
104
+ Instance Attributes:
105
+ channel_id: Database ID of the channel configuration.
106
+ name: Human-readable channel name.
107
+ config: Channel-specific configuration.
108
+ is_active: Whether the channel is active.
109
+
110
+ Example:
111
+ class SlackChannel(BaseNotificationChannel):
112
+ channel_type = "slack"
113
+
114
+ async def send(self, message: str, **kwargs) -> bool:
115
+ webhook_url = self.config["webhook_url"]
116
+ # Send to Slack...
117
+ return True
118
+ """
119
+
120
+ channel_type: ClassVar[str] = "base"
121
+
122
+ def __init__(
123
+ self,
124
+ channel_id: str,
125
+ name: str,
126
+ config: dict[str, Any],
127
+ is_active: bool = True,
128
+ ) -> None:
129
+ """Initialize the channel.
130
+
131
+ Args:
132
+ channel_id: Database ID of the channel.
133
+ name: Human-readable channel name.
134
+ config: Channel-specific configuration.
135
+ is_active: Whether the channel is active.
136
+ """
137
+ self.channel_id = channel_id
138
+ self.name = name
139
+ self.config = config
140
+ self.is_active = is_active
141
+
142
+ @classmethod
143
+ @abstractmethod
144
+ def get_config_schema(cls) -> dict[str, Any]:
145
+ """Get the configuration schema for this channel type.
146
+
147
+ Returns a JSON Schema-like dictionary describing required
148
+ and optional configuration fields.
149
+
150
+ Returns:
151
+ Configuration schema dictionary.
152
+
153
+ Example:
154
+ return {
155
+ "webhook_url": {
156
+ "type": "string",
157
+ "required": True,
158
+ "description": "Slack webhook URL",
159
+ },
160
+ }
161
+ """
162
+ ...
163
+
164
+ @classmethod
165
+ def validate_config(cls, config: dict[str, Any]) -> list[str]:
166
+ """Validate channel configuration.
167
+
168
+ Args:
169
+ config: Configuration to validate.
170
+
171
+ Returns:
172
+ List of validation error messages (empty if valid).
173
+ """
174
+ errors = []
175
+ schema = cls.get_config_schema()
176
+
177
+ for field_name, field_schema in schema.items():
178
+ if field_schema.get("required", False) and field_name not in config:
179
+ errors.append(f"Missing required field: {field_name}")
180
+ elif field_name in config:
181
+ expected_type = field_schema.get("type")
182
+ value = config[field_name]
183
+
184
+ if expected_type == "string" and not isinstance(value, str):
185
+ errors.append(f"Field '{field_name}' must be a string")
186
+ elif expected_type == "integer" and not isinstance(value, int):
187
+ errors.append(f"Field '{field_name}' must be an integer")
188
+ elif expected_type == "boolean" and not isinstance(value, bool):
189
+ errors.append(f"Field '{field_name}' must be a boolean")
190
+ elif expected_type == "array" and not isinstance(value, list):
191
+ errors.append(f"Field '{field_name}' must be an array")
192
+ elif expected_type == "object" and not isinstance(value, dict):
193
+ errors.append(f"Field '{field_name}' must be an object")
194
+
195
+ return errors
196
+
197
+ @abstractmethod
198
+ async def send(
199
+ self,
200
+ message: str,
201
+ event: NotificationEvent | None = None,
202
+ **kwargs: Any,
203
+ ) -> bool:
204
+ """Send a notification through this channel.
205
+
206
+ Args:
207
+ message: The message to send.
208
+ event: Optional event that triggered this notification.
209
+ **kwargs: Additional channel-specific options.
210
+
211
+ Returns:
212
+ True if the notification was sent successfully.
213
+ """
214
+ ...
215
+
216
+ async def send_with_result(
217
+ self,
218
+ message: str,
219
+ event: NotificationEvent | None = None,
220
+ **kwargs: Any,
221
+ ) -> NotificationResult:
222
+ """Send notification and return detailed result.
223
+
224
+ This is a template method that wraps send() with error handling
225
+ and result construction.
226
+
227
+ Args:
228
+ message: The message to send.
229
+ event: Optional event that triggered this notification.
230
+ **kwargs: Additional channel-specific options.
231
+
232
+ Returns:
233
+ NotificationResult with delivery details.
234
+ """
235
+ try:
236
+ success = await self.send(message, event=event, **kwargs)
237
+ return NotificationResult(
238
+ success=success,
239
+ channel_id=self.channel_id,
240
+ channel_type=self.channel_type,
241
+ message=message,
242
+ error=None if success else "Send returned False",
243
+ metadata={"event_type": event.event_type if event else None},
244
+ )
245
+ except Exception as e:
246
+ return NotificationResult(
247
+ success=False,
248
+ channel_id=self.channel_id,
249
+ channel_type=self.channel_type,
250
+ message=message,
251
+ error=str(e),
252
+ metadata={"event_type": event.event_type if event else None},
253
+ )
254
+
255
+ def format_message(self, event: NotificationEvent) -> str:
256
+ """Format a notification message for this channel.
257
+
258
+ Override to customize message formatting per channel type.
259
+
260
+ Args:
261
+ event: The event to format.
262
+
263
+ Returns:
264
+ Formatted message string.
265
+ """
266
+ return self._default_format(event)
267
+
268
+ def _default_format(self, event: NotificationEvent) -> str:
269
+ """Default message formatting."""
270
+ source_info = f" for {event.source_name}" if event.source_name else ""
271
+ return f"[{event.event_type}]{source_info}: {event.data}"
272
+
273
+
274
+ class ChannelRegistry:
275
+ """Registry for notification channel types.
276
+
277
+ Provides a plugin system for registering and discovering
278
+ channel implementations.
279
+
280
+ Usage:
281
+ # Register a channel
282
+ @ChannelRegistry.register("custom")
283
+ class CustomChannel(BaseNotificationChannel):
284
+ ...
285
+
286
+ # Get a channel class
287
+ channel_class = ChannelRegistry.get("custom")
288
+
289
+ # Create an instance
290
+ channel = ChannelRegistry.create(
291
+ "custom",
292
+ channel_id="123",
293
+ name="My Channel",
294
+ config={...},
295
+ )
296
+ """
297
+
298
+ _channels: ClassVar[dict[str, type[BaseNotificationChannel]]] = {}
299
+
300
+ @classmethod
301
+ def register(
302
+ cls, channel_type: str
303
+ ) -> type[type[BaseNotificationChannel]]:
304
+ """Decorator to register a channel implementation.
305
+
306
+ Args:
307
+ channel_type: Unique identifier for this channel type.
308
+
309
+ Returns:
310
+ Decorator function.
311
+
312
+ Example:
313
+ @ChannelRegistry.register("slack")
314
+ class SlackChannel(BaseNotificationChannel):
315
+ ...
316
+ """
317
+
318
+ def decorator(
319
+ channel_class: type[BaseNotificationChannel],
320
+ ) -> type[BaseNotificationChannel]:
321
+ channel_class.channel_type = channel_type
322
+ cls._channels[channel_type] = channel_class
323
+ return channel_class
324
+
325
+ return decorator # type: ignore
326
+
327
+ @classmethod
328
+ def get(cls, channel_type: str) -> type[BaseNotificationChannel] | None:
329
+ """Get a registered channel class by type.
330
+
331
+ Args:
332
+ channel_type: Channel type identifier.
333
+
334
+ Returns:
335
+ Channel class or None if not found.
336
+ """
337
+ return cls._channels.get(channel_type)
338
+
339
+ @classmethod
340
+ def create(
341
+ cls,
342
+ channel_type: str,
343
+ channel_id: str,
344
+ name: str,
345
+ config: dict[str, Any],
346
+ is_active: bool = True,
347
+ ) -> BaseNotificationChannel | None:
348
+ """Create a channel instance by type.
349
+
350
+ Args:
351
+ channel_type: Channel type identifier.
352
+ channel_id: Database ID.
353
+ name: Channel name.
354
+ config: Channel configuration.
355
+ is_active: Whether channel is active.
356
+
357
+ Returns:
358
+ Channel instance or None if type not found.
359
+ """
360
+ channel_class = cls.get(channel_type)
361
+ if channel_class is None:
362
+ return None
363
+
364
+ return channel_class(
365
+ channel_id=channel_id,
366
+ name=name,
367
+ config=config,
368
+ is_active=is_active,
369
+ )
370
+
371
+ @classmethod
372
+ def list_types(cls) -> list[str]:
373
+ """Get list of registered channel types.
374
+
375
+ Returns:
376
+ List of channel type identifiers.
377
+ """
378
+ return list(cls._channels.keys())
379
+
380
+ @classmethod
381
+ def get_all_schemas(cls) -> dict[str, dict[str, Any]]:
382
+ """Get configuration schemas for all registered channels.
383
+
384
+ Returns:
385
+ Dictionary mapping channel type to config schema.
386
+ """
387
+ return {
388
+ channel_type: channel_class.get_config_schema()
389
+ for channel_type, channel_class in cls._channels.items()
390
+ }