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,479 @@
1
+ """Extensible caching system with multiple backend support.
2
+
3
+ This module provides a flexible caching abstraction that supports
4
+ multiple backends (memory, file-based) with consistent interface.
5
+
6
+ The cache system uses the Strategy pattern for backend flexibility
7
+ and supports TTL-based expiration.
8
+
9
+ Example:
10
+ cache = get_cache()
11
+ await cache.set("key", {"data": "value"}, ttl=60)
12
+ value = await cache.get("key")
13
+ await cache.delete("key")
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import hashlib
20
+ import json
21
+ import logging
22
+ from abc import ABC, abstractmethod
23
+ from datetime import datetime, timedelta
24
+ from pathlib import Path
25
+ from typing import Any, Generic, TypeVar
26
+
27
+ from truthound_dashboard.config import get_settings
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ T = TypeVar("T")
32
+
33
+
34
+ class CacheEntry(Generic[T]):
35
+ """Cache entry with value and expiration.
36
+
37
+ Attributes:
38
+ value: Cached value.
39
+ expires_at: Expiration datetime.
40
+ created_at: Creation datetime.
41
+ """
42
+
43
+ __slots__ = ("value", "expires_at", "created_at")
44
+
45
+ def __init__(self, value: T, ttl_seconds: int) -> None:
46
+ """Initialize cache entry.
47
+
48
+ Args:
49
+ value: Value to cache.
50
+ ttl_seconds: Time to live in seconds.
51
+ """
52
+ self.value = value
53
+ self.created_at = datetime.utcnow()
54
+ self.expires_at = self.created_at + timedelta(seconds=ttl_seconds)
55
+
56
+ @property
57
+ def is_expired(self) -> bool:
58
+ """Check if entry has expired."""
59
+ return datetime.utcnow() >= self.expires_at
60
+
61
+ @property
62
+ def remaining_ttl(self) -> int:
63
+ """Get remaining TTL in seconds."""
64
+ delta = self.expires_at - datetime.utcnow()
65
+ return max(0, int(delta.total_seconds()))
66
+
67
+
68
+ class CacheBackend(ABC):
69
+ """Abstract base class for cache backends.
70
+
71
+ Subclass this to implement custom cache storage backends.
72
+ """
73
+
74
+ @abstractmethod
75
+ async def get(self, key: str) -> Any | None:
76
+ """Get value from cache.
77
+
78
+ Args:
79
+ key: Cache key.
80
+
81
+ Returns:
82
+ Cached value or None if not found/expired.
83
+ """
84
+ ...
85
+
86
+ @abstractmethod
87
+ async def set(self, key: str, value: Any, ttl_seconds: int = 60) -> None:
88
+ """Set value in cache with TTL.
89
+
90
+ Args:
91
+ key: Cache key.
92
+ value: Value to cache.
93
+ ttl_seconds: Time to live in seconds.
94
+ """
95
+ ...
96
+
97
+ @abstractmethod
98
+ async def delete(self, key: str) -> bool:
99
+ """Delete value from cache.
100
+
101
+ Args:
102
+ key: Cache key.
103
+
104
+ Returns:
105
+ True if key existed and was deleted.
106
+ """
107
+ ...
108
+
109
+ @abstractmethod
110
+ async def clear(self) -> None:
111
+ """Clear all cached values."""
112
+ ...
113
+
114
+ @abstractmethod
115
+ async def exists(self, key: str) -> bool:
116
+ """Check if key exists and is not expired.
117
+
118
+ Args:
119
+ key: Cache key.
120
+
121
+ Returns:
122
+ True if key exists and is valid.
123
+ """
124
+ ...
125
+
126
+ async def get_or_set(
127
+ self,
128
+ key: str,
129
+ factory: Any,
130
+ ttl_seconds: int = 60,
131
+ ) -> Any:
132
+ """Get value from cache or compute and cache it.
133
+
134
+ Args:
135
+ key: Cache key.
136
+ factory: Callable or coroutine to compute value if not cached.
137
+ ttl_seconds: Time to live in seconds.
138
+
139
+ Returns:
140
+ Cached or computed value.
141
+ """
142
+ value = await self.get(key)
143
+ if value is not None:
144
+ return value
145
+
146
+ # Compute value
147
+ if asyncio.iscoroutinefunction(factory):
148
+ value = await factory()
149
+ elif callable(factory):
150
+ value = factory()
151
+ else:
152
+ value = factory
153
+
154
+ await self.set(key, value, ttl_seconds)
155
+ return value
156
+
157
+ async def invalidate_pattern(self, pattern: str) -> int:
158
+ """Invalidate all keys matching pattern.
159
+
160
+ Default implementation does nothing. Override for pattern support.
161
+
162
+ Args:
163
+ pattern: Pattern to match (implementation-specific).
164
+
165
+ Returns:
166
+ Number of keys invalidated.
167
+ """
168
+ return 0
169
+
170
+
171
+ class MemoryCache(CacheBackend):
172
+ """Thread-safe in-memory cache with TTL support.
173
+
174
+ Uses asyncio.Lock for thread-safety in async context.
175
+ Includes automatic cleanup of expired entries.
176
+ """
177
+
178
+ def __init__(self, max_size: int = 1000, cleanup_interval: int = 300) -> None:
179
+ """Initialize memory cache.
180
+
181
+ Args:
182
+ max_size: Maximum number of entries to store.
183
+ cleanup_interval: Interval for cleanup task in seconds.
184
+ """
185
+ self._cache: dict[str, CacheEntry[Any]] = {}
186
+ self._lock = asyncio.Lock()
187
+ self._max_size = max_size
188
+ self._cleanup_interval = cleanup_interval
189
+ self._cleanup_task: asyncio.Task[None] | None = None
190
+
191
+ async def start_cleanup_task(self) -> None:
192
+ """Start background cleanup task."""
193
+ if self._cleanup_task is None:
194
+ self._cleanup_task = asyncio.create_task(self._cleanup_loop())
195
+
196
+ async def stop_cleanup_task(self) -> None:
197
+ """Stop background cleanup task."""
198
+ if self._cleanup_task is not None:
199
+ self._cleanup_task.cancel()
200
+ try:
201
+ await self._cleanup_task
202
+ except asyncio.CancelledError:
203
+ pass
204
+ self._cleanup_task = None
205
+
206
+ async def _cleanup_loop(self) -> None:
207
+ """Background cleanup loop."""
208
+ while True:
209
+ try:
210
+ await asyncio.sleep(self._cleanup_interval)
211
+ await self._cleanup_expired()
212
+ except asyncio.CancelledError:
213
+ break
214
+ except Exception as e:
215
+ logger.error(f"Cache cleanup error: {e}")
216
+
217
+ async def _cleanup_expired(self) -> int:
218
+ """Remove expired entries.
219
+
220
+ Returns:
221
+ Number of entries removed.
222
+ """
223
+ async with self._lock:
224
+ expired_keys = [
225
+ key for key, entry in self._cache.items() if entry.is_expired
226
+ ]
227
+ for key in expired_keys:
228
+ del self._cache[key]
229
+ return len(expired_keys)
230
+
231
+ async def _evict_if_needed(self) -> None:
232
+ """Evict oldest entries if cache is full."""
233
+ if len(self._cache) >= self._max_size:
234
+ # Remove oldest 10% of entries
235
+ to_remove = max(1, self._max_size // 10)
236
+ sorted_entries = sorted(
237
+ self._cache.items(),
238
+ key=lambda x: x[1].created_at,
239
+ )
240
+ for key, _ in sorted_entries[:to_remove]:
241
+ del self._cache[key]
242
+
243
+ async def get(self, key: str) -> Any | None:
244
+ """Get value from cache."""
245
+ async with self._lock:
246
+ entry = self._cache.get(key)
247
+ if entry is None:
248
+ return None
249
+ if entry.is_expired:
250
+ del self._cache[key]
251
+ return None
252
+ return entry.value
253
+
254
+ async def set(self, key: str, value: Any, ttl_seconds: int = 60) -> None:
255
+ """Set value in cache with TTL."""
256
+ async with self._lock:
257
+ await self._evict_if_needed()
258
+ self._cache[key] = CacheEntry(value, ttl_seconds)
259
+
260
+ async def delete(self, key: str) -> bool:
261
+ """Delete value from cache."""
262
+ async with self._lock:
263
+ if key in self._cache:
264
+ del self._cache[key]
265
+ return True
266
+ return False
267
+
268
+ async def clear(self) -> None:
269
+ """Clear all cached values."""
270
+ async with self._lock:
271
+ self._cache.clear()
272
+
273
+ async def exists(self, key: str) -> bool:
274
+ """Check if key exists and is not expired."""
275
+ async with self._lock:
276
+ entry = self._cache.get(key)
277
+ if entry is None:
278
+ return False
279
+ if entry.is_expired:
280
+ del self._cache[key]
281
+ return False
282
+ return True
283
+
284
+ async def invalidate_pattern(self, pattern: str) -> int:
285
+ """Invalidate all keys matching pattern (prefix match)."""
286
+ async with self._lock:
287
+ keys_to_remove = [key for key in self._cache if key.startswith(pattern)]
288
+ for key in keys_to_remove:
289
+ del self._cache[key]
290
+ return len(keys_to_remove)
291
+
292
+ @property
293
+ def size(self) -> int:
294
+ """Get current cache size."""
295
+ return len(self._cache)
296
+
297
+ async def get_stats(self) -> dict[str, Any]:
298
+ """Get cache statistics."""
299
+ async with self._lock:
300
+ expired_count = sum(1 for e in self._cache.values() if e.is_expired)
301
+ return {
302
+ "total_entries": len(self._cache),
303
+ "expired_entries": expired_count,
304
+ "valid_entries": len(self._cache) - expired_count,
305
+ "max_size": self._max_size,
306
+ }
307
+
308
+
309
+ class FileCache(CacheBackend):
310
+ """File-based cache with TTL support.
311
+
312
+ Stores cache entries as JSON files in a directory.
313
+ Suitable for data that should persist across restarts.
314
+ """
315
+
316
+ def __init__(self, cache_dir: Path | None = None) -> None:
317
+ """Initialize file cache.
318
+
319
+ Args:
320
+ cache_dir: Directory for cache files. Defaults to settings.cache_dir.
321
+ """
322
+ self._cache_dir = cache_dir or get_settings().cache_dir
323
+ self._cache_dir.mkdir(parents=True, exist_ok=True)
324
+ self._lock = asyncio.Lock()
325
+
326
+ def _get_cache_path(self, key: str) -> Path:
327
+ """Get file path for cache key."""
328
+ # Use hash for safe filename
329
+ key_hash = hashlib.sha256(key.encode()).hexdigest()[:32]
330
+ return self._cache_dir / f"{key_hash}.cache"
331
+
332
+ async def get(self, key: str) -> Any | None:
333
+ """Get value from cache."""
334
+ cache_path = self._get_cache_path(key)
335
+
336
+ async with self._lock:
337
+ if not cache_path.exists():
338
+ return None
339
+
340
+ try:
341
+ data = json.loads(cache_path.read_text())
342
+ expires_at = datetime.fromisoformat(data["expires_at"])
343
+
344
+ if datetime.utcnow() >= expires_at:
345
+ cache_path.unlink(missing_ok=True)
346
+ return None
347
+
348
+ return data["value"]
349
+ except (json.JSONDecodeError, KeyError, ValueError):
350
+ cache_path.unlink(missing_ok=True)
351
+ return None
352
+
353
+ async def set(self, key: str, value: Any, ttl_seconds: int = 60) -> None:
354
+ """Set value in cache with TTL."""
355
+ cache_path = self._get_cache_path(key)
356
+ expires_at = datetime.utcnow() + timedelta(seconds=ttl_seconds)
357
+
358
+ async with self._lock:
359
+ data = {
360
+ "key": key,
361
+ "value": value,
362
+ "expires_at": expires_at.isoformat(),
363
+ "created_at": datetime.utcnow().isoformat(),
364
+ }
365
+ cache_path.write_text(json.dumps(data))
366
+
367
+ async def delete(self, key: str) -> bool:
368
+ """Delete value from cache."""
369
+ cache_path = self._get_cache_path(key)
370
+
371
+ async with self._lock:
372
+ if cache_path.exists():
373
+ cache_path.unlink()
374
+ return True
375
+ return False
376
+
377
+ async def clear(self) -> None:
378
+ """Clear all cached values."""
379
+ async with self._lock:
380
+ for cache_file in self._cache_dir.glob("*.cache"):
381
+ cache_file.unlink(missing_ok=True)
382
+
383
+ async def exists(self, key: str) -> bool:
384
+ """Check if key exists and is not expired."""
385
+ return await self.get(key) is not None
386
+
387
+
388
+ class CacheManager:
389
+ """Manager for multiple cache instances with namespacing.
390
+
391
+ Provides a unified interface for managing multiple caches
392
+ with different configurations and backends.
393
+ """
394
+
395
+ def __init__(self) -> None:
396
+ """Initialize cache manager."""
397
+ self._caches: dict[str, CacheBackend] = {}
398
+ self._default_backend: type[CacheBackend] = MemoryCache
399
+
400
+ def register(
401
+ self,
402
+ name: str,
403
+ backend: CacheBackend | None = None,
404
+ ) -> CacheBackend:
405
+ """Register a cache with the manager.
406
+
407
+ Args:
408
+ name: Cache name/namespace.
409
+ backend: Cache backend instance. Defaults to MemoryCache.
410
+
411
+ Returns:
412
+ Registered cache backend.
413
+ """
414
+ if name not in self._caches:
415
+ self._caches[name] = backend or self._default_backend()
416
+ return self._caches[name]
417
+
418
+ def get(self, name: str) -> CacheBackend | None:
419
+ """Get a registered cache.
420
+
421
+ Args:
422
+ name: Cache name/namespace.
423
+
424
+ Returns:
425
+ Cache backend or None if not registered.
426
+ """
427
+ return self._caches.get(name)
428
+
429
+ def get_or_create(self, name: str) -> CacheBackend:
430
+ """Get or create a cache.
431
+
432
+ Args:
433
+ name: Cache name/namespace.
434
+
435
+ Returns:
436
+ Cache backend.
437
+ """
438
+ return self.register(name)
439
+
440
+ async def clear_all(self) -> None:
441
+ """Clear all registered caches."""
442
+ for cache in self._caches.values():
443
+ await cache.clear()
444
+
445
+
446
+ # Singleton instances
447
+ _cache: MemoryCache | None = None
448
+ _cache_manager: CacheManager | None = None
449
+
450
+
451
+ def get_cache() -> MemoryCache:
452
+ """Get default cache singleton.
453
+
454
+ Returns:
455
+ MemoryCache instance.
456
+ """
457
+ global _cache
458
+ if _cache is None:
459
+ _cache = MemoryCache()
460
+ return _cache
461
+
462
+
463
+ def get_cache_manager() -> CacheManager:
464
+ """Get cache manager singleton.
465
+
466
+ Returns:
467
+ CacheManager instance.
468
+ """
469
+ global _cache_manager
470
+ if _cache_manager is None:
471
+ _cache_manager = CacheManager()
472
+ return _cache_manager
473
+
474
+
475
+ def reset_cache() -> None:
476
+ """Reset cache singletons (for testing)."""
477
+ global _cache, _cache_manager
478
+ _cache = None
479
+ _cache_manager = None