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