Coverage for src / agent / services / maven / cache.py: 100%

77 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-11 14:30 +0000

1# Copyright 2025-2026 Microsoft Corporation 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14 

15"""TTL-based in-memory cache service for Maven API responses. 

16 

17This module provides a simple, efficient caching mechanism for Maven Central 

18API responses with configurable TTL support. Cache keys follow patterns like: 

19- metadata:{group_id}:{artifact_id} 

20- versions:{group_id}:{artifact_id} 

21- exists:{group_id}:{artifact_id}:{version} 

22""" 

23 

24import logging 

25import time 

26from dataclasses import dataclass 

27from typing import Any 

28 

29logger = logging.getLogger(__name__) 

30 

31 

32@dataclass 

33class CacheEntry: 

34 """Single cache entry with value and expiration time.""" 

35 

36 value: Any 

37 expires_at: float 

38 

39 

40class MavenCacheService: 

41 """In-memory cache with TTL support for Maven API responses. 

42 

43 Thread-safe cache implementation using simple dictionary storage. 

44 Expired entries are cleaned up on access to prevent memory leaks. 

45 

46 Default TTLs: 

47 - Metadata: 3600 seconds (1 hour) 

48 - Search results: 900 seconds (15 minutes) 

49 - Artifact existence: 3600 seconds (1 hour) 

50 """ 

51 

52 # Default TTL values in seconds 

53 DEFAULT_METADATA_TTL = 3600 # 1 hour 

54 DEFAULT_SEARCH_TTL = 900 # 15 minutes 

55 DEFAULT_EXISTS_TTL = 3600 # 1 hour 

56 

57 def __init__(self, default_ttl: int = 3600) -> None: 

58 """Initialize cache service. 

59 

60 Args: 

61 default_ttl: Default TTL in seconds for cache entries (default: 1 hour) 

62 """ 

63 self._cache: dict[str, CacheEntry] = {} 

64 self._default_ttl = default_ttl 

65 logger.debug("MavenCacheService initialized with default TTL: %ds", default_ttl) 

66 

67 def get(self, key: str) -> Any | None: 

68 """Get value from cache if not expired. 

69 

70 Args: 

71 key: Cache key to look up 

72 

73 Returns: 

74 Cached value if exists and not expired, None otherwise 

75 """ 

76 entry = self._cache.get(key) 

77 if entry is None: 

78 return None 

79 

80 if time.time() > entry.expires_at: 

81 # Entry expired, remove it 

82 del self._cache[key] 

83 logger.debug("Cache entry expired: %s", key) 

84 return None 

85 

86 logger.debug("Cache hit: %s", key) 

87 return entry.value 

88 

89 def set(self, key: str, value: Any, ttl: int | None = None) -> None: 

90 """Set value in cache with TTL. 

91 

92 Args: 

93 key: Cache key 

94 value: Value to cache 

95 ttl: TTL in seconds (uses default if not specified) 

96 """ 

97 ttl_seconds = ttl if ttl is not None else self._default_ttl 

98 expires_at = time.time() + ttl_seconds 

99 self._cache[key] = CacheEntry(value=value, expires_at=expires_at) 

100 logger.debug("Cache set: %s (TTL: %ds)", key, ttl_seconds) 

101 

102 def delete(self, key: str) -> bool: 

103 """Delete a specific cache entry. 

104 

105 Args: 

106 key: Cache key to delete 

107 

108 Returns: 

109 True if entry was deleted, False if not found 

110 """ 

111 if key in self._cache: 

112 del self._cache[key] 

113 logger.debug("Cache entry deleted: %s", key) 

114 return True 

115 return False 

116 

117 def clear(self) -> int: 

118 """Clear all cache entries. 

119 

120 Returns: 

121 Number of entries cleared 

122 """ 

123 count = len(self._cache) 

124 self._cache.clear() 

125 logger.debug("Cache cleared: %d entries", count) 

126 return count 

127 

128 def cleanup_expired(self) -> int: 

129 """Remove all expired entries from cache. 

130 

131 Returns: 

132 Number of expired entries removed 

133 """ 

134 current_time = time.time() 

135 expired_keys = [ 

136 key for key, entry in self._cache.items() if current_time > entry.expires_at 

137 ] 

138 

139 for key in expired_keys: 

140 del self._cache[key] 

141 

142 if expired_keys: 

143 logger.debug("Cleaned up %d expired cache entries", len(expired_keys)) 

144 

145 return len(expired_keys) 

146 

147 def size(self) -> int: 

148 """Get current cache size. 

149 

150 Returns: 

151 Number of entries in cache (including expired ones) 

152 """ 

153 return len(self._cache) 

154 

155 # Convenience methods for Maven-specific cache operations 

156 

157 def get_metadata(self, group_id: str, artifact_id: str) -> Any | None: 

158 """Get cached Maven metadata. 

159 

160 Args: 

161 group_id: Maven group ID 

162 artifact_id: Maven artifact ID 

163 

164 Returns: 

165 Cached metadata or None 

166 """ 

167 key = f"metadata:{group_id}:{artifact_id}" 

168 return self.get(key) 

169 

170 def set_metadata(self, group_id: str, artifact_id: str, metadata: Any) -> None: 

171 """Cache Maven metadata. 

172 

173 Args: 

174 group_id: Maven group ID 

175 artifact_id: Maven artifact ID 

176 metadata: Metadata to cache 

177 """ 

178 key = f"metadata:{group_id}:{artifact_id}" 

179 self.set(key, metadata, ttl=self.DEFAULT_METADATA_TTL) 

180 

181 def get_versions(self, group_id: str, artifact_id: str) -> list[str] | None: 

182 """Get cached version list. 

183 

184 Args: 

185 group_id: Maven group ID 

186 artifact_id: Maven artifact ID 

187 

188 Returns: 

189 Cached version list or None 

190 """ 

191 key = f"versions:{group_id}:{artifact_id}" 

192 return self.get(key) 

193 

194 def set_versions(self, group_id: str, artifact_id: str, versions: list[str]) -> None: 

195 """Cache version list. 

196 

197 Args: 

198 group_id: Maven group ID 

199 artifact_id: Maven artifact ID 

200 versions: List of versions to cache 

201 """ 

202 key = f"versions:{group_id}:{artifact_id}" 

203 self.set(key, versions, ttl=self.DEFAULT_METADATA_TTL) 

204 

205 def get_exists( 

206 self, 

207 group_id: str, 

208 artifact_id: str, 

209 version: str, 

210 packaging: str = "jar", 

211 classifier: str | None = None, 

212 ) -> bool | None: 

213 """Get cached artifact existence check. 

214 

215 Args: 

216 group_id: Maven group ID 

217 artifact_id: Maven artifact ID 

218 version: Version string 

219 packaging: Package type 

220 classifier: Optional classifier 

221 

222 Returns: 

223 Cached existence result or None 

224 """ 

225 key = f"exists:{group_id}:{artifact_id}:{version}:{packaging}:{classifier}" 

226 return self.get(key) 

227 

228 def set_exists( 

229 self, 

230 group_id: str, 

231 artifact_id: str, 

232 version: str, 

233 exists: bool, 

234 packaging: str = "jar", 

235 classifier: str | None = None, 

236 ) -> None: 

237 """Cache artifact existence check. 

238 

239 Args: 

240 group_id: Maven group ID 

241 artifact_id: Maven artifact ID 

242 version: Version string 

243 exists: Whether artifact exists 

244 packaging: Package type 

245 classifier: Optional classifier 

246 """ 

247 key = f"exists:{group_id}:{artifact_id}:{version}:{packaging}:{classifier}" 

248 self.set(key, exists, ttl=self.DEFAULT_EXISTS_TTL) 

249 

250 def get_search(self, query: str) -> Any | None: 

251 """Get cached search results. 

252 

253 Args: 

254 query: Search query string 

255 

256 Returns: 

257 Cached search results or None 

258 """ 

259 key = f"search:{query}" 

260 return self.get(key) 

261 

262 def set_search(self, query: str, results: Any) -> None: 

263 """Cache search results. 

264 

265 Args: 

266 query: Search query string 

267 results: Search results to cache 

268 """ 

269 key = f"search:{query}" 

270 self.set(key, results, ttl=self.DEFAULT_SEARCH_TTL)