mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
164 lines
4.2 KiB
Python
164 lines
4.2 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# Copyright (c) 2025 Allaun
|
|
|
|
"""Database Cache Module.
|
|
|
|
This module provides caching functionality for database query results.
|
|
|
|
Classes:
|
|
DatabaseCache: In-memory cache for database operations.
|
|
|
|
Example:
|
|
>>> from nodupe.core.database import Database
|
|
>>> db = Database("/path/to/db.db")
|
|
>>> db.cache.set("key", "value")
|
|
>>> db.cache.get("key")
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, Optional
|
|
import time
|
|
|
|
|
|
class DatabaseCache:
|
|
"""In-memory database cache.
|
|
|
|
Provides caching for database query results to improve performance.
|
|
|
|
Attributes:
|
|
max_size: Maximum number of cached entries.
|
|
ttl: Time-to-live for cached entries in seconds.
|
|
|
|
Example:
|
|
>>> cache = DatabaseCache(connection)
|
|
>>> cache.set("query_1", [{"id": 1}])
|
|
>>> cache.get("query_1")
|
|
"""
|
|
|
|
def __init__(self, connection: Any, max_size: int = 1000, ttl: float = 300.0) -> None:
|
|
"""Initialize database cache.
|
|
|
|
Args:
|
|
connection: Database connection instance.
|
|
max_size: Maximum cache size.
|
|
ttl: Time-to-live in seconds.
|
|
"""
|
|
self.connection = connection
|
|
self._cache: Dict[str, tuple[Any, float]] = {}
|
|
self.max_size = max_size
|
|
self.ttl = ttl
|
|
|
|
def get(self, key: str) -> Optional[Any]:
|
|
"""Get cached value.
|
|
|
|
Args:
|
|
key: Cache key.
|
|
|
|
Returns:
|
|
Cached value or None if not found or expired.
|
|
|
|
Example:
|
|
>>> cache.get("query_1")
|
|
[{'id': 1}]
|
|
"""
|
|
if key not in self._cache:
|
|
return None
|
|
|
|
value, timestamp = self._cache[key]
|
|
|
|
# Check TTL
|
|
if time.time() - timestamp > self.ttl:
|
|
del self._cache[key]
|
|
return None
|
|
|
|
return value
|
|
|
|
def set(self, key: str, value: Any) -> None:
|
|
"""Set cached value.
|
|
|
|
Args:
|
|
key: Cache key.
|
|
value: Value to cache.
|
|
|
|
Example:
|
|
>>> cache.set("query_1", [{"id": 1}])
|
|
"""
|
|
# Evict oldest if at capacity
|
|
if len(self._cache) >= self.max_size:
|
|
oldest_key = min(self._cache.keys(), key=lambda k: self._cache[k][1])
|
|
del self._cache[oldest_key]
|
|
|
|
self._cache[key] = (value, time.time())
|
|
|
|
def clear(self) -> None:
|
|
"""Clear all cached values.
|
|
|
|
Example:
|
|
>>> cache.clear()
|
|
"""
|
|
self._cache.clear()
|
|
|
|
def delete(self, key: str) -> bool:
|
|
"""Delete a cached value.
|
|
|
|
Args:
|
|
key: Cache key.
|
|
|
|
Returns:
|
|
True if key was deleted, False if not found.
|
|
|
|
Example:
|
|
>>> cache.delete("query_1")
|
|
"""
|
|
if key in self._cache:
|
|
del self._cache[key]
|
|
return True
|
|
return False
|
|
|
|
def size(self) -> int:
|
|
"""Get current cache size.
|
|
|
|
Returns:
|
|
Number of cached entries.
|
|
|
|
Example:
|
|
>>> cache.size()
|
|
10
|
|
"""
|
|
return len(self._cache)
|
|
|
|
def cleanup_expired(self) -> int:
|
|
"""Remove expired or malformed cache entries.
|
|
|
|
Returns:
|
|
Number of entries removed.
|
|
|
|
This is a conservative, test-friendly implementation: it tolerates
|
|
missing or malformed timestamps and only removes items whose TTL
|
|
has clearly expired.
|
|
"""
|
|
now = time.time()
|
|
removed = 0
|
|
keys = list(self._cache.keys())
|
|
for k in keys:
|
|
try:
|
|
value, ts = self._cache.get(k, (None, None))
|
|
if ts is None:
|
|
# Malformed entry: remove it
|
|
del self._cache[k]
|
|
removed += 1
|
|
continue
|
|
|
|
if now - ts > self.ttl:
|
|
del self._cache[k]
|
|
removed += 1
|
|
except KeyError:
|
|
# Concurrent modification or already removed
|
|
continue
|
|
except Exception:
|
|
# Be defensive in tests: don't let unexpected errors
|
|
# during cleanup fail the caller; just skip the key.
|
|
continue
|
|
|
|
return removed
|