Coverage for .claude/hooks/moai/lib/config_cache.py: 0.00%
91 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-19 08:00 +0900
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-19 08:00 +0900
1#!/usr/bin/env python3
2"""Singleton configuration cache for Alfred hooks
4Provides efficient caching of frequently accessed configuration data
5with automatic invalidation based on file modification time.
7Features:
8- Singleton pattern for global cache state
9- File mtime-based cache invalidation
10- Type-safe cache operations
11- Graceful degradation on read errors
12"""
14import json
15from datetime import datetime, timedelta
16from pathlib import Path
17from typing import Any, Optional
20class ConfigCache:
21 """Singleton cache for configuration data.
23 Stores commonly accessed configuration to avoid repeated file I/O.
24 Automatically invalidates cached data if source file is modified.
26 Usage:
27 cache = ConfigCache()
28 config = cache.get_config()
29 spec_progress = cache.get_spec_progress()
30 """
32 _instance = None
33 _cache = {}
34 _mtimes = {}
36 def __new__(cls):
37 if cls._instance is None:
38 cls._instance = super().__new__(cls)
39 return cls._instance
41 @staticmethod
42 def get_config() -> Optional[dict[str, Any]]:
43 """Get cached config, or load from file if not cached.
45 Returns:
46 Configuration dict, or None if file doesn't exist
47 """
48 config_path = Path.cwd() / ".moai" / "config" / "config.json"
50 # Check if cache is still valid
51 if ConfigCache._is_cache_valid("config", config_path):
52 return ConfigCache._cache.get("config")
54 # Load from file
55 try:
56 if not config_path.exists():
57 return None
59 config = json.loads(config_path.read_text())
60 ConfigCache._update_cache("config", config, config_path)
61 return config
63 except Exception:
64 return None
66 @staticmethod
67 def get_spec_progress() -> dict[str, Any]:
68 """Get cached SPEC progress, or compute if not cached.
70 Returns:
71 Dict with keys: completed, total, percentage
72 """
73 specs_dir = Path.cwd() / ".moai" / "specs"
75 # Check if cache is still valid (5 minute TTL)
76 if ConfigCache._is_cache_valid("spec_progress", specs_dir, ttl_minutes=5):
77 return ConfigCache._cache.get("spec_progress", {"completed": 0, "total": 0, "percentage": 0})
79 # Compute from filesystem
80 try:
81 if not specs_dir.exists():
82 result = {"completed": 0, "total": 0, "percentage": 0}
83 ConfigCache._update_cache("spec_progress", result, specs_dir)
84 return result
86 spec_folders = [d for d in specs_dir.iterdir() if d.is_dir() and d.name.startswith("SPEC-")]
87 total = len(spec_folders)
89 # Simple completion check - look for spec.md files
90 completed = sum(1 for folder in spec_folders if (folder / "spec.md").exists())
92 percentage = (completed / total * 100) if total > 0 else 0
94 result = {
95 "completed": completed,
96 "total": total,
97 "percentage": round(percentage, 0)
98 }
100 ConfigCache._update_cache("spec_progress", result, specs_dir)
101 return result
103 except Exception:
104 return {"completed": 0, "total": 0, "percentage": 0}
106 @staticmethod
107 def _is_cache_valid(key: str, file_path: Path, ttl_minutes: int = 30) -> bool:
108 """Check if cached data is still valid.
110 Args:
111 key: Cache key
112 file_path: Path to check for modifications
113 ttl_minutes: Time-to-live in minutes
115 Returns:
116 True if cache exists and is still valid
117 """
118 if key not in ConfigCache._cache:
119 return False
121 if not file_path.exists():
122 return False
124 # Check file modification time
125 try:
126 current_mtime = file_path.stat().st_mtime
127 cached_mtime = ConfigCache._mtimes.get(key)
129 if cached_mtime is None:
130 return False
132 # If file was modified, cache is invalid
133 if current_mtime != cached_mtime:
134 return False
136 # Check TTL
137 cached_time = ConfigCache._cache.get(f"{key}_timestamp")
138 if cached_time:
139 elapsed = datetime.now() - cached_time
140 if elapsed > timedelta(minutes=ttl_minutes):
141 return False
143 return True
145 except Exception:
146 return False
148 @staticmethod
149 def _update_cache(key: str, data: Any, file_path: Path) -> None:
150 """Update cache with new data.
152 Args:
153 key: Cache key
154 data: Data to cache
155 file_path: Path to track for modifications
156 """
157 try:
158 ConfigCache._cache[key] = data
159 ConfigCache._cache[f"{key}_timestamp"] = datetime.now()
161 if file_path.exists():
162 ConfigCache._mtimes[key] = file_path.stat().st_mtime
164 except Exception:
165 pass # Silently fail on cache update
167 @staticmethod
168 def clear() -> None:
169 """Clear all cached data.
171 Useful for testing or forcing a refresh.
172 """
173 ConfigCache._cache.clear()
174 ConfigCache._mtimes.clear()
176 @staticmethod
177 def get_cache_size() -> int:
178 """Get current cache size in bytes.
180 Returns:
181 Approximate size of cached data in bytes
182 """
183 import sys
184 size = sys.getsizeof(ConfigCache._cache) + sys.getsizeof(ConfigCache._mtimes)
185 for key, value in ConfigCache._cache.items():
186 size += sys.getsizeof(value)
187 return size
190# Convenience functions for singleton access
191def get_cached_config() -> Optional[dict[str, Any]]:
192 """Get cached configuration."""
193 return ConfigCache.get_config()
196def get_cached_spec_progress() -> dict[str, Any]:
197 """Get cached SPEC progress."""
198 return ConfigCache.get_spec_progress()
201def clear_config_cache() -> None:
202 """Clear all cached data."""
203 ConfigCache.clear()