Coverage for .claude/hooks/moai/lib/version_cache.py: 0.00%
60 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"""Version information cache with TTL support
4TTL-based caching system for version check results to minimize network calls
5during SessionStart hook execution.
7SPEC: SPEC-UPDATE-ENHANCE-001 - SessionStart version check system enhancement
8Phase 1: Cache System Implementation
9"""
11import json
12from datetime import datetime, timezone
13from pathlib import Path
14from typing import Any
17class VersionCache:
18 """TTL-based version information cache
20 Caches version check results with configurable Time-To-Live (TTL)
21 to avoid excessive network calls to PyPI during SessionStart events.
23 Attributes:
24 cache_dir: Directory to store cache file
25 ttl_hours: Time-to-live in hours (default 24)
26 cache_file: Path to the cache JSON file
28 Examples:
29 >>> cache = VersionCache(Path(".moai/cache"), ttl_hours=24)
30 >>> cache.save({"current_version": "0.8.1", "latest_version": "0.9.0"})
31 True
32 >>> cache.is_valid()
33 True
34 >>> data = cache.load()
35 >>> data["current_version"]
36 '0.8.1'
37 """
39 def __init__(self, cache_dir: Path, ttl_hours: int = 4):
40 """Initialize cache with TTL in hours
42 Args:
43 cache_dir: Directory where cache file will be stored
44 ttl_hours: Time-to-live in hours (default 4)
45 """
46 self.cache_dir = Path(cache_dir)
47 self.ttl_hours = ttl_hours
48 self.cache_file = self.cache_dir / "version-check.json"
50 def _calculate_age_hours(self, last_check_iso: str) -> float:
51 """Calculate age in hours from ISO timestamp (internal helper)
53 Normalizes timezone-aware and naive datetimes for consistent comparison.
55 Args:
56 last_check_iso: ISO format timestamp string
58 Returns:
59 Age in hours
61 Raises:
62 ValueError: If timestamp parsing fails
63 """
64 last_check = datetime.fromisoformat(last_check_iso)
66 # Normalize to naive datetime (remove timezone for comparison)
67 if last_check.tzinfo is not None:
68 last_check = last_check.replace(tzinfo=None)
70 now = datetime.now()
71 return (now - last_check).total_seconds() / 3600
73 def is_valid(self) -> bool:
74 """Check if cache exists and is not expired
76 Returns:
77 True if cache file exists and is within TTL, False otherwise
79 Examples:
80 >>> cache = VersionCache(Path(".moai/cache"))
81 >>> cache.is_valid()
82 False # No cache file exists yet
83 """
84 if not self.cache_file.exists():
85 return False
87 try:
88 with open(self.cache_file, "r") as f:
89 data = json.load(f)
91 age_hours = self._calculate_age_hours(data["last_check"])
92 return age_hours < self.ttl_hours
94 except (json.JSONDecodeError, KeyError, ValueError, OSError):
95 # Corrupted or invalid cache file
96 return False
98 def load(self) -> dict[str, Any] | None:
99 """Load cached version info if valid
101 Returns:
102 Cached version info dictionary if valid, None otherwise
104 Examples:
105 >>> cache = VersionCache(Path(".moai/cache"))
106 >>> data = cache.load()
107 >>> data is None
108 True # No valid cache exists
109 """
110 if not self.is_valid():
111 return None
113 try:
114 with open(self.cache_file, "r") as f:
115 return json.load(f)
116 except (json.JSONDecodeError, OSError):
117 # Graceful degradation on read errors
118 return None
120 def save(self, version_info: dict[str, Any]) -> bool:
121 """Save version info to cache file
123 Creates cache directory if it doesn't exist.
124 Updates last_check timestamp to current time if not provided.
126 Args:
127 version_info: Version information dictionary to cache
129 Returns:
130 True on successful save, False on error
132 Examples:
133 >>> cache = VersionCache(Path(".moai/cache"))
134 >>> cache.save({"current_version": "0.8.1"})
135 True
136 """
137 try:
138 # Create cache directory if it doesn't exist
139 self.cache_dir.mkdir(parents=True, exist_ok=True)
141 # Update last_check timestamp only if not provided (for testing)
142 if "last_check" not in version_info:
143 version_info["last_check"] = datetime.now(timezone.utc).isoformat()
145 # Write to cache file
146 with open(self.cache_file, "w") as f:
147 json.dump(version_info, f, indent=2)
149 return True
151 except (OSError, TypeError):
152 # Graceful degradation on write errors
153 return False
155 def clear(self) -> bool:
156 """Clear/remove cache file
158 Returns:
159 True if cache file was removed or didn't exist, False on error
161 Examples:
162 >>> cache = VersionCache(Path(".moai/cache"))
163 >>> cache.clear()
164 True
165 """
166 try:
167 if self.cache_file.exists():
168 self.cache_file.unlink()
169 return True
170 except OSError:
171 return False
173 def get_age_hours(self) -> float:
174 """Get age of cache in hours
176 Returns:
177 Age in hours, or 0.0 if cache doesn't exist or is invalid
179 Examples:
180 >>> cache = VersionCache(Path(".moai/cache"))
181 >>> cache.get_age_hours()
182 0.0 # No cache exists
183 """
184 if not self.cache_file.exists():
185 return 0.0
187 try:
188 with open(self.cache_file, "r") as f:
189 data = json.load(f)
191 return self._calculate_age_hours(data["last_check"])
193 except (json.JSONDecodeError, KeyError, ValueError, OSError):
194 return 0.0
197__all__ = ["VersionCache"]