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,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
|
+
}
|