Coverage for src / moai_adk / core / performance / cache_system.py: 0.00%
148 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
1"""
2Cache System
4Provides persistent caching capabilities with TTL support for improved performance.
5"""
7import json
8import os
9import time
10from typing import Any, Dict, List, Optional
13class CacheSystem:
14 """
15 A persistent cache system with TTL support.
17 This class provides file-based caching with support for time-to-live,
18 multiple operations, persistence across instances, and thread safety.
19 """
21 def __init__(self, cache_dir: Optional[str] = None, auto_cleanup: bool = True):
22 """
23 Initialize the cache system.
25 Args:
26 cache_dir: Directory to store cache files. If None, uses default temp directory.
27 auto_cleanup: Whether to automatically clean up expired files on operations
28 """
29 self.auto_cleanup = auto_cleanup
31 if cache_dir is None:
32 import tempfile
34 self.cache_dir = os.path.join(tempfile.gettempdir(), "moai_adk_cache")
35 else:
36 self.cache_dir = cache_dir
38 # Cache file extension
39 self.file_extension = ".cache"
41 # Create cache directory if it doesn't exist
42 self._ensure_cache_dir()
44 def _ensure_cache_dir(self) -> None:
45 """Ensure cache directory exists."""
46 try:
47 os.makedirs(self.cache_dir, exist_ok=True)
48 except OSError as e:
49 raise OSError(f"Failed to create cache directory {self.cache_dir}: {e}")
51 def _validate_key(self, key: str) -> str:
52 """
53 Validate and sanitize cache key.
55 Args:
56 key: Raw cache key
58 Returns:
59 Sanitized key suitable for filename
60 """
61 if not isinstance(key, str):
62 raise TypeError("Cache key must be a string")
64 if not key or key.isspace():
65 raise ValueError("Cache key cannot be empty")
67 # Sanitize key for safe filename usage
68 safe_key = key.replace("/", "_").replace("\\", "_")
69 if safe_key != key:
70 return safe_key
72 return key
74 def _get_file_path(self, key: str) -> str:
75 """Get file path for a given cache key."""
76 safe_key = self._validate_key(key)
77 return os.path.join(self.cache_dir, f"{safe_key}{self.file_extension}")
79 def _is_expired(self, data: Dict[str, Any]) -> bool:
80 """Check if cache data is expired."""
81 if "expires_at" not in data:
82 return False
84 return time.time() > data["expires_at"]
86 def _cleanup_expired_files(self) -> None:
87 """Remove expired cache files."""
88 time.time()
89 for file_name in os.listdir(self.cache_dir):
90 if file_name.endswith(self.file_extension):
91 file_path = os.path.join(self.cache_dir, file_name)
92 try:
93 with open(file_path, "r", encoding="utf-8") as f:
94 data = json.load(f)
96 if self._is_expired(data):
97 os.remove(file_path)
98 except (json.JSONDecodeError, KeyError, OSError):
99 # Remove corrupted files too
100 try:
101 os.remove(file_path)
102 except OSError:
103 pass
105 def _write_data(self, file_path: str, data: Dict[str, Any]) -> None:
106 """Write data to file with error handling."""
107 try:
108 with open(file_path, "w", encoding="utf-8") as f:
109 json.dump(data, f, indent=2, ensure_ascii=False)
110 except (OSError, TypeError) as e:
111 raise OSError(f"Failed to write cache file {file_path}: {e}")
113 def _read_data(self, file_path: str) -> Optional[Dict[str, Any]]:
114 """Read data from file with error handling."""
115 if not os.path.exists(file_path):
116 return None
118 try:
119 with open(file_path, "r", encoding="utf-8") as f:
120 return json.load(f)
121 except (json.JSONDecodeError, OSError):
122 # File is corrupted, remove it
123 try:
124 os.remove(file_path)
125 except OSError:
126 pass
127 return None
129 def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
130 """
131 Set a value in the cache.
133 Args:
134 key: Cache key
135 value: Value to cache (must be JSON serializable)
136 ttl: Time to live in seconds (optional)
138 Raises:
139 TypeError: If value is not JSON serializable
140 OSError: If file operations fail
141 """
142 # Validate JSON serializability
143 try:
144 json.dumps(value)
145 except (TypeError, ValueError) as e:
146 raise TypeError(f"Cache value must be JSON serializable: {e}")
148 data = {"value": value, "created_at": time.time()}
150 if ttl is not None:
151 if not isinstance(ttl, (int, float)) or ttl < 0:
152 raise ValueError("TTL must be a positive number")
153 data["expires_at"] = data["created_at"] + ttl
155 file_path = self._get_file_path(key)
156 self._write_data(file_path, data)
158 # Auto-cleanup if enabled
159 if self.auto_cleanup:
160 self._cleanup_expired_files()
162 def get(self, key: str) -> Optional[Any]:
163 """
164 Get a value from the cache.
166 Args:
167 key: Cache key
169 Returns:
170 Cached value or None if not found or expired
171 """
172 file_path = self._get_file_path(key)
173 data = self._read_data(file_path)
175 if data is None:
176 return None
178 # Check expiration
179 if self._is_expired(data):
180 try:
181 os.remove(file_path)
182 except OSError:
183 pass
184 return None
186 return data["value"]
188 def delete(self, key: str) -> bool:
189 """
190 Delete a value from the cache.
192 Args:
193 key: Cache key
195 Returns:
196 True if file was deleted, False if it didn't exist
197 """
198 file_path = self._get_file_path(key)
199 try:
200 os.remove(file_path)
201 return True
202 except OSError:
203 return False
205 def clear(self) -> int:
206 """
207 Clear all values from the cache.
209 Returns:
210 Number of files removed
211 """
212 count = 0
213 for file_name in os.listdir(self.cache_dir):
214 if file_name.endswith(self.file_extension):
215 file_path = os.path.join(self.cache_dir, file_name)
216 try:
217 os.remove(file_path)
218 count += 1
219 except OSError:
220 continue
221 return count
223 def exists(self, key: str) -> bool:
224 """
225 Check if a key exists in the cache.
227 Args:
228 key: Cache key
230 Returns:
231 True if key exists and is not expired, False otherwise
232 """
233 return self.get(key) is not None
235 def size(self) -> int:
236 """
237 Get the number of items in the cache.
239 Returns:
240 Number of non-expired cache items
241 """
242 count = 0
243 for file_name in os.listdir(self.cache_dir):
244 if file_name.endswith(self.file_extension):
245 os.path.join(self.cache_dir, file_name)
246 key = file_name[: -len(self.file_extension)] # Remove extension
248 if self.exists(key):
249 count += 1
250 return count
252 def set_if_not_exists(
253 self, key: str, value: Any, ttl: Optional[float] = None
254 ) -> bool:
255 """
256 Set a value only if the key doesn't exist.
258 Args:
259 key: Cache key
260 value: Value to cache
261 ttl: Time to live in seconds (optional)
263 Returns:
264 True if value was set, False if key already existed
265 """
266 if self.exists(key):
267 return False
269 self.set(key, value, ttl)
270 return True
272 def get_multiple(self, keys: List[str]) -> Dict[str, Optional[Any]]:
273 """
274 Get multiple values from the cache.
276 Args:
277 keys: List of cache keys
279 Returns:
280 Dictionary mapping keys to values (or None)
281 """
282 if not isinstance(keys, list):
283 raise TypeError("keys must be a list")
285 result = {}
286 for key in keys:
287 if not isinstance(key, str):
288 raise TypeError("All keys must be strings")
289 result[key] = self.get(key)
290 return result
292 def get_stats(self) -> Dict[str, Any]:
293 """
294 Get cache statistics.
296 Returns:
297 Dictionary with cache statistics
298 """
299 total_files = 0
300 expired_files = 0
301 time.time()
303 for file_name in os.listdir(self.cache_dir):
304 if file_name.endswith(self.file_extension):
305 total_files += 1
306 file_path = os.path.join(self.cache_dir, file_name)
307 data = self._read_data(file_path)
309 if data and self._is_expired(data):
310 expired_files += 1
312 return {
313 "total_files": total_files,
314 "expired_files": expired_files,
315 "valid_files": total_files - expired_files,
316 "cache_directory": self.cache_dir,
317 "auto_cleanup_enabled": self.auto_cleanup,
318 }