truthound-dashboard 1.3.1__py3-none-any.whl → 1.4.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/api/alerts.py +258 -0
- truthound_dashboard/api/anomaly.py +1302 -0
- truthound_dashboard/api/cross_alerts.py +352 -0
- truthound_dashboard/api/deps.py +143 -0
- truthound_dashboard/api/drift_monitor.py +540 -0
- truthound_dashboard/api/lineage.py +1151 -0
- truthound_dashboard/api/maintenance.py +363 -0
- truthound_dashboard/api/middleware.py +373 -1
- truthound_dashboard/api/model_monitoring.py +805 -0
- truthound_dashboard/api/notifications_advanced.py +2452 -0
- truthound_dashboard/api/plugins.py +2096 -0
- truthound_dashboard/api/profile.py +211 -14
- truthound_dashboard/api/reports.py +853 -0
- truthound_dashboard/api/router.py +147 -0
- truthound_dashboard/api/rule_suggestions.py +310 -0
- truthound_dashboard/api/schema_evolution.py +231 -0
- truthound_dashboard/api/sources.py +47 -3
- truthound_dashboard/api/triggers.py +190 -0
- truthound_dashboard/api/validations.py +13 -0
- truthound_dashboard/api/validators.py +333 -4
- truthound_dashboard/api/versioning.py +309 -0
- truthound_dashboard/api/websocket.py +301 -0
- truthound_dashboard/core/__init__.py +27 -0
- truthound_dashboard/core/anomaly.py +1395 -0
- truthound_dashboard/core/anomaly_explainer.py +633 -0
- truthound_dashboard/core/cache.py +206 -0
- truthound_dashboard/core/cached_services.py +422 -0
- truthound_dashboard/core/charts.py +352 -0
- truthound_dashboard/core/connections.py +1069 -42
- truthound_dashboard/core/cross_alerts.py +837 -0
- truthound_dashboard/core/drift_monitor.py +1477 -0
- truthound_dashboard/core/drift_sampling.py +669 -0
- truthound_dashboard/core/i18n/__init__.py +42 -0
- truthound_dashboard/core/i18n/detector.py +173 -0
- truthound_dashboard/core/i18n/messages.py +564 -0
- truthound_dashboard/core/lineage.py +971 -0
- truthound_dashboard/core/maintenance.py +443 -5
- truthound_dashboard/core/model_monitoring.py +1043 -0
- truthound_dashboard/core/notifications/channels.py +1020 -1
- truthound_dashboard/core/notifications/deduplication/__init__.py +143 -0
- truthound_dashboard/core/notifications/deduplication/policies.py +274 -0
- truthound_dashboard/core/notifications/deduplication/service.py +400 -0
- truthound_dashboard/core/notifications/deduplication/stores.py +2365 -0
- truthound_dashboard/core/notifications/deduplication/strategies.py +422 -0
- truthound_dashboard/core/notifications/dispatcher.py +43 -0
- truthound_dashboard/core/notifications/escalation/__init__.py +149 -0
- truthound_dashboard/core/notifications/escalation/backends.py +1384 -0
- truthound_dashboard/core/notifications/escalation/engine.py +429 -0
- truthound_dashboard/core/notifications/escalation/models.py +336 -0
- truthound_dashboard/core/notifications/escalation/scheduler.py +1187 -0
- truthound_dashboard/core/notifications/escalation/state_machine.py +330 -0
- truthound_dashboard/core/notifications/escalation/stores.py +2896 -0
- truthound_dashboard/core/notifications/events.py +49 -0
- truthound_dashboard/core/notifications/metrics/__init__.py +115 -0
- truthound_dashboard/core/notifications/metrics/base.py +528 -0
- truthound_dashboard/core/notifications/metrics/collectors.py +583 -0
- truthound_dashboard/core/notifications/routing/__init__.py +169 -0
- truthound_dashboard/core/notifications/routing/combinators.py +184 -0
- truthound_dashboard/core/notifications/routing/config.py +375 -0
- truthound_dashboard/core/notifications/routing/config_parser.py +867 -0
- truthound_dashboard/core/notifications/routing/engine.py +382 -0
- truthound_dashboard/core/notifications/routing/expression_engine.py +1269 -0
- truthound_dashboard/core/notifications/routing/jinja2_engine.py +774 -0
- truthound_dashboard/core/notifications/routing/rules.py +625 -0
- truthound_dashboard/core/notifications/routing/validator.py +678 -0
- truthound_dashboard/core/notifications/service.py +2 -0
- truthound_dashboard/core/notifications/stats_aggregator.py +850 -0
- truthound_dashboard/core/notifications/throttling/__init__.py +83 -0
- truthound_dashboard/core/notifications/throttling/builder.py +311 -0
- truthound_dashboard/core/notifications/throttling/stores.py +1859 -0
- truthound_dashboard/core/notifications/throttling/throttlers.py +633 -0
- truthound_dashboard/core/openlineage.py +1028 -0
- truthound_dashboard/core/plugins/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/__init__.py +39 -0
- truthound_dashboard/core/plugins/docs/extractor.py +703 -0
- truthound_dashboard/core/plugins/docs/renderers.py +804 -0
- truthound_dashboard/core/plugins/hooks/__init__.py +63 -0
- truthound_dashboard/core/plugins/hooks/decorators.py +367 -0
- truthound_dashboard/core/plugins/hooks/manager.py +403 -0
- truthound_dashboard/core/plugins/hooks/protocols.py +265 -0
- truthound_dashboard/core/plugins/lifecycle/__init__.py +41 -0
- truthound_dashboard/core/plugins/lifecycle/hot_reload.py +584 -0
- truthound_dashboard/core/plugins/lifecycle/machine.py +419 -0
- truthound_dashboard/core/plugins/lifecycle/states.py +266 -0
- truthound_dashboard/core/plugins/loader.py +504 -0
- truthound_dashboard/core/plugins/registry.py +810 -0
- truthound_dashboard/core/plugins/reporter_executor.py +588 -0
- truthound_dashboard/core/plugins/sandbox/__init__.py +59 -0
- truthound_dashboard/core/plugins/sandbox/code_validator.py +243 -0
- truthound_dashboard/core/plugins/sandbox/engines.py +770 -0
- truthound_dashboard/core/plugins/sandbox/protocols.py +194 -0
- truthound_dashboard/core/plugins/sandbox.py +617 -0
- truthound_dashboard/core/plugins/security/__init__.py +68 -0
- truthound_dashboard/core/plugins/security/analyzer.py +535 -0
- truthound_dashboard/core/plugins/security/policies.py +311 -0
- truthound_dashboard/core/plugins/security/protocols.py +296 -0
- truthound_dashboard/core/plugins/security/signing.py +842 -0
- truthound_dashboard/core/plugins/security.py +446 -0
- truthound_dashboard/core/plugins/validator_executor.py +401 -0
- truthound_dashboard/core/plugins/versioning/__init__.py +51 -0
- truthound_dashboard/core/plugins/versioning/constraints.py +377 -0
- truthound_dashboard/core/plugins/versioning/dependencies.py +541 -0
- truthound_dashboard/core/plugins/versioning/semver.py +266 -0
- truthound_dashboard/core/profile_comparison.py +601 -0
- truthound_dashboard/core/report_history.py +570 -0
- truthound_dashboard/core/reporters/__init__.py +57 -0
- truthound_dashboard/core/reporters/base.py +296 -0
- truthound_dashboard/core/reporters/csv_reporter.py +155 -0
- truthound_dashboard/core/reporters/html_reporter.py +598 -0
- truthound_dashboard/core/reporters/i18n/__init__.py +65 -0
- truthound_dashboard/core/reporters/i18n/base.py +494 -0
- truthound_dashboard/core/reporters/i18n/catalogs.py +930 -0
- truthound_dashboard/core/reporters/json_reporter.py +160 -0
- truthound_dashboard/core/reporters/junit_reporter.py +233 -0
- truthound_dashboard/core/reporters/markdown_reporter.py +207 -0
- truthound_dashboard/core/reporters/pdf_reporter.py +209 -0
- truthound_dashboard/core/reporters/registry.py +272 -0
- truthound_dashboard/core/rule_generator.py +2088 -0
- truthound_dashboard/core/scheduler.py +822 -12
- truthound_dashboard/core/schema_evolution.py +858 -0
- truthound_dashboard/core/services.py +152 -9
- truthound_dashboard/core/statistics.py +718 -0
- truthound_dashboard/core/streaming_anomaly.py +883 -0
- truthound_dashboard/core/triggers/__init__.py +45 -0
- truthound_dashboard/core/triggers/base.py +226 -0
- truthound_dashboard/core/triggers/evaluators.py +609 -0
- truthound_dashboard/core/triggers/factory.py +363 -0
- truthound_dashboard/core/unified_alerts.py +870 -0
- truthound_dashboard/core/validation_limits.py +509 -0
- truthound_dashboard/core/versioning.py +709 -0
- truthound_dashboard/core/websocket/__init__.py +59 -0
- truthound_dashboard/core/websocket/manager.py +512 -0
- truthound_dashboard/core/websocket/messages.py +130 -0
- truthound_dashboard/db/__init__.py +30 -0
- truthound_dashboard/db/models.py +3375 -3
- truthound_dashboard/main.py +22 -0
- truthound_dashboard/schemas/__init__.py +396 -1
- truthound_dashboard/schemas/anomaly.py +1258 -0
- truthound_dashboard/schemas/base.py +4 -0
- truthound_dashboard/schemas/cross_alerts.py +334 -0
- truthound_dashboard/schemas/drift_monitor.py +890 -0
- truthound_dashboard/schemas/lineage.py +428 -0
- truthound_dashboard/schemas/maintenance.py +154 -0
- truthound_dashboard/schemas/model_monitoring.py +374 -0
- truthound_dashboard/schemas/notifications_advanced.py +1363 -0
- truthound_dashboard/schemas/openlineage.py +704 -0
- truthound_dashboard/schemas/plugins.py +1293 -0
- truthound_dashboard/schemas/profile.py +420 -34
- truthound_dashboard/schemas/profile_comparison.py +242 -0
- truthound_dashboard/schemas/reports.py +285 -0
- truthound_dashboard/schemas/rule_suggestion.py +434 -0
- truthound_dashboard/schemas/schema_evolution.py +164 -0
- truthound_dashboard/schemas/source.py +117 -2
- truthound_dashboard/schemas/triggers.py +511 -0
- truthound_dashboard/schemas/unified_alerts.py +223 -0
- truthound_dashboard/schemas/validation.py +25 -1
- truthound_dashboard/schemas/validators/__init__.py +11 -0
- truthound_dashboard/schemas/validators/base.py +151 -0
- truthound_dashboard/schemas/versioning.py +152 -0
- truthound_dashboard/static/index.html +2 -2
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/METADATA +142 -22
- truthound_dashboard-1.4.0.dist-info/RECORD +239 -0
- truthound_dashboard/static/assets/index-BZG20KuF.js +0 -586
- truthound_dashboard/static/assets/index-D_HyZ3pb.css +0 -1
- truthound_dashboard/static/assets/unmerged_dictionaries-CtpqQBm0.js +0 -1
- truthound_dashboard-1.3.1.dist-info/RECORD +0 -110
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/WHEEL +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/entry_points.txt +0 -0
- {truthound_dashboard-1.3.1.dist-info → truthound_dashboard-1.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -306,6 +306,212 @@ class MemoryCache(CacheBackend):
|
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
|
|
309
|
+
class LFUCacheEntry(Generic[T]):
|
|
310
|
+
"""LFU Cache entry with frequency tracking.
|
|
311
|
+
|
|
312
|
+
Attributes:
|
|
313
|
+
value: Cached value.
|
|
314
|
+
frequency: Access frequency count.
|
|
315
|
+
expires_at: Expiration datetime.
|
|
316
|
+
created_at: Creation datetime.
|
|
317
|
+
last_accessed: Last access datetime.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
__slots__ = ("value", "frequency", "expires_at", "created_at", "last_accessed")
|
|
321
|
+
|
|
322
|
+
def __init__(self, value: T, ttl_seconds: int) -> None:
|
|
323
|
+
"""Initialize LFU cache entry.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
value: Value to cache.
|
|
327
|
+
ttl_seconds: Time to live in seconds.
|
|
328
|
+
"""
|
|
329
|
+
self.value = value
|
|
330
|
+
self.frequency = 1
|
|
331
|
+
self.created_at = datetime.utcnow()
|
|
332
|
+
self.last_accessed = self.created_at
|
|
333
|
+
self.expires_at = self.created_at + timedelta(seconds=ttl_seconds)
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def is_expired(self) -> bool:
|
|
337
|
+
"""Check if entry has expired."""
|
|
338
|
+
return datetime.utcnow() >= self.expires_at
|
|
339
|
+
|
|
340
|
+
def access(self) -> None:
|
|
341
|
+
"""Record an access to this entry."""
|
|
342
|
+
self.frequency += 1
|
|
343
|
+
self.last_accessed = datetime.utcnow()
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class LFUCache(CacheBackend):
|
|
347
|
+
"""Least Frequently Used (LFU) cache with TTL support.
|
|
348
|
+
|
|
349
|
+
Evicts least frequently accessed entries when cache is full.
|
|
350
|
+
Includes automatic cleanup of expired entries.
|
|
351
|
+
|
|
352
|
+
Features:
|
|
353
|
+
- Frequency-based eviction (least frequently used first)
|
|
354
|
+
- Tie-breaking by last access time (LRU among same frequency)
|
|
355
|
+
- TTL-based expiration
|
|
356
|
+
- Thread-safe with asyncio.Lock
|
|
357
|
+
"""
|
|
358
|
+
|
|
359
|
+
def __init__(self, max_size: int = 1000, cleanup_interval: int = 300) -> None:
|
|
360
|
+
"""Initialize LFU cache.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
max_size: Maximum number of entries to store.
|
|
364
|
+
cleanup_interval: Interval for cleanup task in seconds.
|
|
365
|
+
"""
|
|
366
|
+
self._cache: dict[str, LFUCacheEntry[Any]] = {}
|
|
367
|
+
self._lock = asyncio.Lock()
|
|
368
|
+
self._max_size = max_size
|
|
369
|
+
self._cleanup_interval = cleanup_interval
|
|
370
|
+
self._cleanup_task: asyncio.Task[None] | None = None
|
|
371
|
+
self._hits = 0
|
|
372
|
+
self._misses = 0
|
|
373
|
+
|
|
374
|
+
async def start_cleanup_task(self) -> None:
|
|
375
|
+
"""Start background cleanup task."""
|
|
376
|
+
if self._cleanup_task is None:
|
|
377
|
+
self._cleanup_task = asyncio.create_task(self._cleanup_loop())
|
|
378
|
+
|
|
379
|
+
async def stop_cleanup_task(self) -> None:
|
|
380
|
+
"""Stop background cleanup task."""
|
|
381
|
+
if self._cleanup_task is not None:
|
|
382
|
+
self._cleanup_task.cancel()
|
|
383
|
+
try:
|
|
384
|
+
await self._cleanup_task
|
|
385
|
+
except asyncio.CancelledError:
|
|
386
|
+
pass
|
|
387
|
+
self._cleanup_task = None
|
|
388
|
+
|
|
389
|
+
async def _cleanup_loop(self) -> None:
|
|
390
|
+
"""Background cleanup loop."""
|
|
391
|
+
while True:
|
|
392
|
+
try:
|
|
393
|
+
await asyncio.sleep(self._cleanup_interval)
|
|
394
|
+
await self._cleanup_expired()
|
|
395
|
+
except asyncio.CancelledError:
|
|
396
|
+
break
|
|
397
|
+
except Exception as e:
|
|
398
|
+
logger.error(f"LFU cache cleanup error: {e}")
|
|
399
|
+
|
|
400
|
+
async def _cleanup_expired(self) -> int:
|
|
401
|
+
"""Remove expired entries.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
Number of entries removed.
|
|
405
|
+
"""
|
|
406
|
+
async with self._lock:
|
|
407
|
+
expired_keys = [
|
|
408
|
+
key for key, entry in self._cache.items() if entry.is_expired
|
|
409
|
+
]
|
|
410
|
+
for key in expired_keys:
|
|
411
|
+
del self._cache[key]
|
|
412
|
+
return len(expired_keys)
|
|
413
|
+
|
|
414
|
+
async def _evict_if_needed(self) -> None:
|
|
415
|
+
"""Evict least frequently used entries if cache is full."""
|
|
416
|
+
if len(self._cache) >= self._max_size:
|
|
417
|
+
# Remove 10% of entries with lowest frequency
|
|
418
|
+
to_remove = max(1, self._max_size // 10)
|
|
419
|
+
|
|
420
|
+
# Sort by frequency (ascending), then by last_accessed (ascending)
|
|
421
|
+
sorted_entries = sorted(
|
|
422
|
+
self._cache.items(),
|
|
423
|
+
key=lambda x: (x[1].frequency, x[1].last_accessed),
|
|
424
|
+
)
|
|
425
|
+
for key, _ in sorted_entries[:to_remove]:
|
|
426
|
+
del self._cache[key]
|
|
427
|
+
|
|
428
|
+
async def get(self, key: str) -> Any | None:
|
|
429
|
+
"""Get value from cache and increment frequency."""
|
|
430
|
+
async with self._lock:
|
|
431
|
+
entry = self._cache.get(key)
|
|
432
|
+
if entry is None:
|
|
433
|
+
self._misses += 1
|
|
434
|
+
return None
|
|
435
|
+
if entry.is_expired:
|
|
436
|
+
del self._cache[key]
|
|
437
|
+
self._misses += 1
|
|
438
|
+
return None
|
|
439
|
+
entry.access()
|
|
440
|
+
self._hits += 1
|
|
441
|
+
return entry.value
|
|
442
|
+
|
|
443
|
+
async def set(self, key: str, value: Any, ttl_seconds: int = 60) -> None:
|
|
444
|
+
"""Set value in cache with TTL."""
|
|
445
|
+
async with self._lock:
|
|
446
|
+
await self._evict_if_needed()
|
|
447
|
+
self._cache[key] = LFUCacheEntry(value, ttl_seconds)
|
|
448
|
+
|
|
449
|
+
async def delete(self, key: str) -> bool:
|
|
450
|
+
"""Delete value from cache."""
|
|
451
|
+
async with self._lock:
|
|
452
|
+
if key in self._cache:
|
|
453
|
+
del self._cache[key]
|
|
454
|
+
return True
|
|
455
|
+
return False
|
|
456
|
+
|
|
457
|
+
async def clear(self) -> None:
|
|
458
|
+
"""Clear all cached values."""
|
|
459
|
+
async with self._lock:
|
|
460
|
+
self._cache.clear()
|
|
461
|
+
self._hits = 0
|
|
462
|
+
self._misses = 0
|
|
463
|
+
|
|
464
|
+
async def exists(self, key: str) -> bool:
|
|
465
|
+
"""Check if key exists and is not expired."""
|
|
466
|
+
async with self._lock:
|
|
467
|
+
entry = self._cache.get(key)
|
|
468
|
+
if entry is None:
|
|
469
|
+
return False
|
|
470
|
+
if entry.is_expired:
|
|
471
|
+
del self._cache[key]
|
|
472
|
+
return False
|
|
473
|
+
return True
|
|
474
|
+
|
|
475
|
+
async def invalidate_pattern(self, pattern: str) -> int:
|
|
476
|
+
"""Invalidate all keys matching pattern (prefix match)."""
|
|
477
|
+
async with self._lock:
|
|
478
|
+
keys_to_remove = [key for key in self._cache if key.startswith(pattern)]
|
|
479
|
+
for key in keys_to_remove:
|
|
480
|
+
del self._cache[key]
|
|
481
|
+
return len(keys_to_remove)
|
|
482
|
+
|
|
483
|
+
@property
|
|
484
|
+
def size(self) -> int:
|
|
485
|
+
"""Get current cache size."""
|
|
486
|
+
return len(self._cache)
|
|
487
|
+
|
|
488
|
+
@property
|
|
489
|
+
def hit_rate(self) -> float:
|
|
490
|
+
"""Get cache hit rate."""
|
|
491
|
+
total = self._hits + self._misses
|
|
492
|
+
return self._hits / total if total > 0 else 0.0
|
|
493
|
+
|
|
494
|
+
async def get_stats(self) -> dict[str, Any]:
|
|
495
|
+
"""Get cache statistics."""
|
|
496
|
+
async with self._lock:
|
|
497
|
+
expired_count = sum(1 for e in self._cache.values() if e.is_expired)
|
|
498
|
+
freq_distribution: dict[int, int] = {}
|
|
499
|
+
for entry in self._cache.values():
|
|
500
|
+
freq = entry.frequency
|
|
501
|
+
freq_distribution[freq] = freq_distribution.get(freq, 0) + 1
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
"total_entries": len(self._cache),
|
|
505
|
+
"expired_entries": expired_count,
|
|
506
|
+
"valid_entries": len(self._cache) - expired_count,
|
|
507
|
+
"max_size": self._max_size,
|
|
508
|
+
"hits": self._hits,
|
|
509
|
+
"misses": self._misses,
|
|
510
|
+
"hit_rate": self.hit_rate,
|
|
511
|
+
"frequency_distribution": freq_distribution,
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
|
|
309
515
|
class FileCache(CacheBackend):
|
|
310
516
|
"""File-based cache with TTL support.
|
|
311
517
|
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""Cached service wrappers for improved performance.
|
|
2
|
+
|
|
3
|
+
This module provides caching decorators and cached service classes
|
|
4
|
+
that wrap the base services with cache integration.
|
|
5
|
+
|
|
6
|
+
Cache keys are prefixed by resource type for easy invalidation:
|
|
7
|
+
- validation:{id} - Individual validation results
|
|
8
|
+
- validations:source:{source_id} - Validation list for a source
|
|
9
|
+
- schema:{source_id} - Active schema for a source
|
|
10
|
+
- profile:{source_id} - Latest profile for a source
|
|
11
|
+
- validators - Validator registry (long TTL)
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
from truthound_dashboard.core.cached_services import (
|
|
15
|
+
CachedValidationService,
|
|
16
|
+
invalidate_validation_cache,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
service = CachedValidationService(session)
|
|
20
|
+
validation = await service.get_validation(id) # May return cached
|
|
21
|
+
|
|
22
|
+
# Invalidate cache when data changes
|
|
23
|
+
await invalidate_validation_cache(validation_id=id)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
from collections.abc import Sequence
|
|
30
|
+
from datetime import datetime
|
|
31
|
+
from functools import wraps
|
|
32
|
+
from typing import Any, Callable, TypeVar
|
|
33
|
+
|
|
34
|
+
from truthound_dashboard.core.cache import get_cache, CacheBackend
|
|
35
|
+
from truthound_dashboard.core.versioning import create_version, VersioningStrategy
|
|
36
|
+
from truthound_dashboard.db import Validation
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
# Cache TTL constants (in seconds)
|
|
41
|
+
VALIDATION_TTL = 300 # 5 minutes for individual validations
|
|
42
|
+
VALIDATION_LIST_TTL = 60 # 1 minute for validation lists
|
|
43
|
+
SCHEMA_TTL = 600 # 10 minutes for schemas
|
|
44
|
+
PROFILE_TTL = 600 # 10 minutes for profiles
|
|
45
|
+
VALIDATORS_TTL = 3600 # 1 hour for validator registry
|
|
46
|
+
|
|
47
|
+
# Cache key prefixes
|
|
48
|
+
PREFIX_VALIDATION = "validation:"
|
|
49
|
+
PREFIX_VALIDATIONS_LIST = "validations:source:"
|
|
50
|
+
PREFIX_SCHEMA = "schema:"
|
|
51
|
+
PREFIX_PROFILE = "profile:"
|
|
52
|
+
PREFIX_VALIDATORS = "validators"
|
|
53
|
+
|
|
54
|
+
T = TypeVar("T")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def cache_key(*parts: str) -> str:
|
|
58
|
+
"""Build a cache key from parts.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
parts: Key parts to join.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Cache key string.
|
|
65
|
+
"""
|
|
66
|
+
return ":".join(str(p) for p in parts)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def get_cached_or_compute(
|
|
70
|
+
cache: CacheBackend,
|
|
71
|
+
key: str,
|
|
72
|
+
compute_fn: Callable[[], Any],
|
|
73
|
+
ttl: int = 300,
|
|
74
|
+
) -> Any:
|
|
75
|
+
"""Get value from cache or compute and cache it.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
cache: Cache backend.
|
|
79
|
+
key: Cache key.
|
|
80
|
+
compute_fn: Async function to compute value if not cached.
|
|
81
|
+
ttl: Time to live in seconds.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Cached or computed value.
|
|
85
|
+
"""
|
|
86
|
+
# Try cache first
|
|
87
|
+
cached = await cache.get(key)
|
|
88
|
+
if cached is not None:
|
|
89
|
+
logger.debug(f"Cache hit: {key}")
|
|
90
|
+
return cached
|
|
91
|
+
|
|
92
|
+
# Compute and cache
|
|
93
|
+
logger.debug(f"Cache miss: {key}")
|
|
94
|
+
value = await compute_fn()
|
|
95
|
+
|
|
96
|
+
if value is not None:
|
|
97
|
+
await cache.set(key, value, ttl)
|
|
98
|
+
|
|
99
|
+
return value
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def invalidate_validation_cache(
|
|
103
|
+
*,
|
|
104
|
+
validation_id: str | None = None,
|
|
105
|
+
source_id: str | None = None,
|
|
106
|
+
) -> int:
|
|
107
|
+
"""Invalidate validation-related cache entries.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
validation_id: Specific validation ID to invalidate.
|
|
111
|
+
source_id: Invalidate all caches for a source.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Number of cache entries invalidated.
|
|
115
|
+
"""
|
|
116
|
+
cache = get_cache()
|
|
117
|
+
count = 0
|
|
118
|
+
|
|
119
|
+
if validation_id:
|
|
120
|
+
key = f"{PREFIX_VALIDATION}{validation_id}"
|
|
121
|
+
if await cache.delete(key):
|
|
122
|
+
count += 1
|
|
123
|
+
|
|
124
|
+
if source_id:
|
|
125
|
+
# Invalidate validation list for source
|
|
126
|
+
key = f"{PREFIX_VALIDATIONS_LIST}{source_id}"
|
|
127
|
+
if await cache.delete(key):
|
|
128
|
+
count += 1
|
|
129
|
+
|
|
130
|
+
# Invalidate all validations for source (pattern match)
|
|
131
|
+
count += await cache.invalidate_pattern(f"{PREFIX_VALIDATION}{source_id}")
|
|
132
|
+
|
|
133
|
+
logger.debug(f"Invalidated {count} cache entries")
|
|
134
|
+
return count
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def invalidate_schema_cache(source_id: str) -> bool:
|
|
138
|
+
"""Invalidate schema cache for a source.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
source_id: Source ID.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
True if cache was invalidated.
|
|
145
|
+
"""
|
|
146
|
+
cache = get_cache()
|
|
147
|
+
key = f"{PREFIX_SCHEMA}{source_id}"
|
|
148
|
+
return await cache.delete(key)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def invalidate_profile_cache(source_id: str) -> bool:
|
|
152
|
+
"""Invalidate profile cache for a source.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
source_id: Source ID.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
True if cache was invalidated.
|
|
159
|
+
"""
|
|
160
|
+
cache = get_cache()
|
|
161
|
+
key = f"{PREFIX_PROFILE}{source_id}"
|
|
162
|
+
return await cache.delete(key)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class ValidationCacheService:
|
|
166
|
+
"""Cache wrapper for validation operations.
|
|
167
|
+
|
|
168
|
+
Provides cached access to validation data with automatic
|
|
169
|
+
cache invalidation on writes.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def __init__(self, cache: CacheBackend | None = None) -> None:
|
|
173
|
+
"""Initialize cache service.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
cache: Cache backend. Uses default if not provided.
|
|
177
|
+
"""
|
|
178
|
+
self._cache = cache or get_cache()
|
|
179
|
+
|
|
180
|
+
async def get_validation(
|
|
181
|
+
self,
|
|
182
|
+
validation_id: str,
|
|
183
|
+
compute_fn: Callable[[], Any],
|
|
184
|
+
) -> Validation | None:
|
|
185
|
+
"""Get validation from cache or compute.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
validation_id: Validation ID.
|
|
189
|
+
compute_fn: Async function to get validation if not cached.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Validation or None.
|
|
193
|
+
"""
|
|
194
|
+
key = f"{PREFIX_VALIDATION}{validation_id}"
|
|
195
|
+
return await get_cached_or_compute(
|
|
196
|
+
self._cache,
|
|
197
|
+
key,
|
|
198
|
+
compute_fn,
|
|
199
|
+
VALIDATION_TTL,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
async def get_validations_for_source(
|
|
203
|
+
self,
|
|
204
|
+
source_id: str,
|
|
205
|
+
compute_fn: Callable[[], Any],
|
|
206
|
+
) -> Sequence[Validation]:
|
|
207
|
+
"""Get validations for source from cache or compute.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
source_id: Source ID.
|
|
211
|
+
compute_fn: Async function to get validations if not cached.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Sequence of validations.
|
|
215
|
+
"""
|
|
216
|
+
key = f"{PREFIX_VALIDATIONS_LIST}{source_id}"
|
|
217
|
+
result = await get_cached_or_compute(
|
|
218
|
+
self._cache,
|
|
219
|
+
key,
|
|
220
|
+
compute_fn,
|
|
221
|
+
VALIDATION_LIST_TTL,
|
|
222
|
+
)
|
|
223
|
+
return result or []
|
|
224
|
+
|
|
225
|
+
async def invalidate(
|
|
226
|
+
self,
|
|
227
|
+
*,
|
|
228
|
+
validation_id: str | None = None,
|
|
229
|
+
source_id: str | None = None,
|
|
230
|
+
) -> int:
|
|
231
|
+
"""Invalidate cached validation data.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
validation_id: Specific validation to invalidate.
|
|
235
|
+
source_id: Invalidate all for a source.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Number of entries invalidated.
|
|
239
|
+
"""
|
|
240
|
+
return await invalidate_validation_cache(
|
|
241
|
+
validation_id=validation_id,
|
|
242
|
+
source_id=source_id,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class SchemaCacheService:
|
|
247
|
+
"""Cache wrapper for schema operations."""
|
|
248
|
+
|
|
249
|
+
def __init__(self, cache: CacheBackend | None = None) -> None:
|
|
250
|
+
"""Initialize cache service."""
|
|
251
|
+
self._cache = cache or get_cache()
|
|
252
|
+
|
|
253
|
+
async def get_schema(
|
|
254
|
+
self,
|
|
255
|
+
source_id: str,
|
|
256
|
+
compute_fn: Callable[[], Any],
|
|
257
|
+
) -> Any:
|
|
258
|
+
"""Get schema from cache or compute.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
source_id: Source ID.
|
|
262
|
+
compute_fn: Async function to get schema if not cached.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Schema or None.
|
|
266
|
+
"""
|
|
267
|
+
key = f"{PREFIX_SCHEMA}{source_id}"
|
|
268
|
+
return await get_cached_or_compute(
|
|
269
|
+
self._cache,
|
|
270
|
+
key,
|
|
271
|
+
compute_fn,
|
|
272
|
+
SCHEMA_TTL,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
async def invalidate(self, source_id: str) -> bool:
|
|
276
|
+
"""Invalidate schema cache for a source."""
|
|
277
|
+
return await invalidate_schema_cache(source_id)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class ProfileCacheService:
|
|
281
|
+
"""Cache wrapper for profile operations."""
|
|
282
|
+
|
|
283
|
+
def __init__(self, cache: CacheBackend | None = None) -> None:
|
|
284
|
+
"""Initialize cache service."""
|
|
285
|
+
self._cache = cache or get_cache()
|
|
286
|
+
|
|
287
|
+
async def get_profile(
|
|
288
|
+
self,
|
|
289
|
+
source_id: str,
|
|
290
|
+
compute_fn: Callable[[], Any],
|
|
291
|
+
) -> Any:
|
|
292
|
+
"""Get profile from cache or compute.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
source_id: Source ID.
|
|
296
|
+
compute_fn: Async function to get profile if not cached.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Profile or None.
|
|
300
|
+
"""
|
|
301
|
+
key = f"{PREFIX_PROFILE}{source_id}"
|
|
302
|
+
return await get_cached_or_compute(
|
|
303
|
+
self._cache,
|
|
304
|
+
key,
|
|
305
|
+
compute_fn,
|
|
306
|
+
PROFILE_TTL,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
async def invalidate(self, source_id: str) -> bool:
|
|
310
|
+
"""Invalidate profile cache for a source."""
|
|
311
|
+
return await invalidate_profile_cache(source_id)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class ValidatorsCacheService:
|
|
315
|
+
"""Cache wrapper for validator registry."""
|
|
316
|
+
|
|
317
|
+
def __init__(self, cache: CacheBackend | None = None) -> None:
|
|
318
|
+
"""Initialize cache service."""
|
|
319
|
+
self._cache = cache or get_cache()
|
|
320
|
+
|
|
321
|
+
async def get_validators(
|
|
322
|
+
self,
|
|
323
|
+
compute_fn: Callable[[], Any],
|
|
324
|
+
) -> list[dict[str, Any]]:
|
|
325
|
+
"""Get validators from cache or compute.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
compute_fn: Async function to get validators if not cached.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
List of validator definitions.
|
|
332
|
+
"""
|
|
333
|
+
result = await get_cached_or_compute(
|
|
334
|
+
self._cache,
|
|
335
|
+
PREFIX_VALIDATORS,
|
|
336
|
+
compute_fn,
|
|
337
|
+
VALIDATORS_TTL,
|
|
338
|
+
)
|
|
339
|
+
return result or []
|
|
340
|
+
|
|
341
|
+
async def invalidate(self) -> bool:
|
|
342
|
+
"""Invalidate validators cache."""
|
|
343
|
+
return await self._cache.delete(PREFIX_VALIDATORS)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# Convenience function to create a versioned validation result
|
|
347
|
+
async def create_versioned_validation(
|
|
348
|
+
validation: Validation,
|
|
349
|
+
strategy: VersioningStrategy = VersioningStrategy.INCREMENTAL,
|
|
350
|
+
) -> None:
|
|
351
|
+
"""Create a version entry for a validation result.
|
|
352
|
+
|
|
353
|
+
This integrates versioning with the validation workflow.
|
|
354
|
+
Call after a validation is complete to track its version.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
validation: Completed validation model.
|
|
358
|
+
strategy: Versioning strategy to use.
|
|
359
|
+
"""
|
|
360
|
+
if validation.status not in ("success", "failed"):
|
|
361
|
+
return # Only version completed validations
|
|
362
|
+
|
|
363
|
+
await create_version(
|
|
364
|
+
validation_id=validation.id,
|
|
365
|
+
source_id=validation.source_id,
|
|
366
|
+
result_json=validation.result_json,
|
|
367
|
+
strategy=strategy,
|
|
368
|
+
metadata={
|
|
369
|
+
"passed": validation.passed,
|
|
370
|
+
"total_issues": validation.total_issues,
|
|
371
|
+
"status": validation.status,
|
|
372
|
+
},
|
|
373
|
+
)
|
|
374
|
+
logger.debug(f"Created version for validation {validation.id}")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# Singleton instances for convenience
|
|
378
|
+
_validation_cache: ValidationCacheService | None = None
|
|
379
|
+
_schema_cache: SchemaCacheService | None = None
|
|
380
|
+
_profile_cache: ProfileCacheService | None = None
|
|
381
|
+
_validators_cache: ValidatorsCacheService | None = None
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def get_validation_cache() -> ValidationCacheService:
|
|
385
|
+
"""Get singleton validation cache service."""
|
|
386
|
+
global _validation_cache
|
|
387
|
+
if _validation_cache is None:
|
|
388
|
+
_validation_cache = ValidationCacheService()
|
|
389
|
+
return _validation_cache
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def get_schema_cache() -> SchemaCacheService:
|
|
393
|
+
"""Get singleton schema cache service."""
|
|
394
|
+
global _schema_cache
|
|
395
|
+
if _schema_cache is None:
|
|
396
|
+
_schema_cache = SchemaCacheService()
|
|
397
|
+
return _schema_cache
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def get_profile_cache() -> ProfileCacheService:
|
|
401
|
+
"""Get singleton profile cache service."""
|
|
402
|
+
global _profile_cache
|
|
403
|
+
if _profile_cache is None:
|
|
404
|
+
_profile_cache = ProfileCacheService()
|
|
405
|
+
return _profile_cache
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def get_validators_cache() -> ValidatorsCacheService:
|
|
409
|
+
"""Get singleton validators cache service."""
|
|
410
|
+
global _validators_cache
|
|
411
|
+
if _validators_cache is None:
|
|
412
|
+
_validators_cache = ValidatorsCacheService()
|
|
413
|
+
return _validators_cache
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def reset_cache_services() -> None:
|
|
417
|
+
"""Reset all cache service singletons (for testing)."""
|
|
418
|
+
global _validation_cache, _schema_cache, _profile_cache, _validators_cache
|
|
419
|
+
_validation_cache = None
|
|
420
|
+
_schema_cache = None
|
|
421
|
+
_profile_cache = None
|
|
422
|
+
_validators_cache = None
|