auto-coder 0.1.397__py3-none-any.whl → 0.1.399__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of auto-coder might be problematic. Click here for more details.
- auto_coder-0.1.399.dist-info/METADATA +396 -0
- {auto_coder-0.1.397.dist-info → auto_coder-0.1.399.dist-info}/RECORD +81 -28
- {auto_coder-0.1.397.dist-info → auto_coder-0.1.399.dist-info}/WHEEL +1 -1
- {auto_coder-0.1.397.dist-info → auto_coder-0.1.399.dist-info}/entry_points.txt +2 -0
- autocoder/agent/base_agentic/base_agent.py +2 -2
- autocoder/agent/base_agentic/tools/replace_in_file_tool_resolver.py +1 -1
- autocoder/agent/entry_command_agent/__init__.py +29 -0
- autocoder/agent/entry_command_agent/auto_tool.py +61 -0
- autocoder/agent/entry_command_agent/chat.py +475 -0
- autocoder/agent/entry_command_agent/designer.py +53 -0
- autocoder/agent/entry_command_agent/generate_command.py +50 -0
- autocoder/agent/entry_command_agent/project_reader.py +58 -0
- autocoder/agent/entry_command_agent/voice2text.py +71 -0
- autocoder/auto_coder.py +23 -548
- autocoder/auto_coder_rag.py +1 -0
- autocoder/auto_coder_runner.py +510 -8
- autocoder/chat/rules_command.py +1 -1
- autocoder/chat_auto_coder.py +8 -0
- autocoder/common/ac_style_command_parser/__init__.py +15 -0
- autocoder/common/ac_style_command_parser/example.py +7 -0
- autocoder/{command_parser.py → common/ac_style_command_parser/parser.py} +1 -33
- autocoder/common/ac_style_command_parser/test_parser.py +516 -0
- autocoder/common/command_completer_v2.py +1 -1
- autocoder/common/command_file_manager/examples.py +22 -8
- autocoder/common/command_file_manager/manager.py +37 -6
- autocoder/common/conversations/__init__.py +84 -39
- autocoder/common/conversations/backup/__init__.py +14 -0
- autocoder/common/conversations/backup/backup_manager.py +564 -0
- autocoder/common/conversations/backup/restore_manager.py +546 -0
- autocoder/common/conversations/cache/__init__.py +16 -0
- autocoder/common/conversations/cache/base_cache.py +89 -0
- autocoder/common/conversations/cache/cache_manager.py +368 -0
- autocoder/common/conversations/cache/memory_cache.py +224 -0
- autocoder/common/conversations/config.py +195 -0
- autocoder/common/conversations/exceptions.py +72 -0
- autocoder/common/conversations/file_locker.py +145 -0
- autocoder/common/conversations/get_conversation_manager.py +143 -0
- autocoder/common/conversations/manager.py +1028 -0
- autocoder/common/conversations/models.py +154 -0
- autocoder/common/conversations/search/__init__.py +15 -0
- autocoder/common/conversations/search/filter_manager.py +431 -0
- autocoder/common/conversations/search/text_searcher.py +366 -0
- autocoder/common/conversations/storage/__init__.py +16 -0
- autocoder/common/conversations/storage/base_storage.py +82 -0
- autocoder/common/conversations/storage/file_storage.py +267 -0
- autocoder/common/conversations/storage/index_manager.py +406 -0
- autocoder/common/v2/agent/agentic_edit.py +131 -18
- autocoder/common/v2/agent/agentic_edit_types.py +10 -0
- autocoder/common/v2/code_auto_generate_editblock.py +10 -2
- autocoder/dispacher/__init__.py +10 -0
- autocoder/rags.py +73 -50
- autocoder/run_context.py +1 -0
- autocoder/sdk/__init__.py +188 -0
- autocoder/sdk/cli/__init__.py +15 -0
- autocoder/sdk/cli/__main__.py +26 -0
- autocoder/sdk/cli/completion_wrapper.py +38 -0
- autocoder/sdk/cli/formatters.py +211 -0
- autocoder/sdk/cli/handlers.py +174 -0
- autocoder/sdk/cli/install_completion.py +301 -0
- autocoder/sdk/cli/main.py +284 -0
- autocoder/sdk/cli/options.py +72 -0
- autocoder/sdk/constants.py +102 -0
- autocoder/sdk/core/__init__.py +20 -0
- autocoder/sdk/core/auto_coder_core.py +867 -0
- autocoder/sdk/core/bridge.py +497 -0
- autocoder/sdk/example.py +0 -0
- autocoder/sdk/exceptions.py +72 -0
- autocoder/sdk/models/__init__.py +19 -0
- autocoder/sdk/models/messages.py +209 -0
- autocoder/sdk/models/options.py +194 -0
- autocoder/sdk/models/responses.py +311 -0
- autocoder/sdk/session/__init__.py +32 -0
- autocoder/sdk/session/session.py +106 -0
- autocoder/sdk/session/session_manager.py +56 -0
- autocoder/sdk/utils/__init__.py +24 -0
- autocoder/sdk/utils/formatters.py +216 -0
- autocoder/sdk/utils/io_utils.py +302 -0
- autocoder/sdk/utils/validators.py +287 -0
- autocoder/version.py +2 -1
- auto_coder-0.1.397.dist-info/METADATA +0 -111
- autocoder/common/conversations/compatibility.py +0 -303
- autocoder/common/conversations/conversation_manager.py +0 -502
- autocoder/common/conversations/example.py +0 -152
- {auto_coder-0.1.397.dist-info → auto_coder-0.1.399.dist-info/licenses}/LICENSE +0 -0
- {auto_coder-0.1.397.dist-info → auto_coder-0.1.399.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cache manager for conversation and message caching.
|
|
3
|
+
|
|
4
|
+
This module provides a high-level interface for managing caches of
|
|
5
|
+
conversations and messages, with support for cache warming, invalidation,
|
|
6
|
+
and statistics reporting.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Optional, List, Dict, Any, Callable
|
|
11
|
+
|
|
12
|
+
from .base_cache import BaseCache
|
|
13
|
+
from .memory_cache import MemoryCache
|
|
14
|
+
from ..models import Conversation, ConversationMessage
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CacheManager:
|
|
20
|
+
"""High-level cache manager for conversations and messages."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
conversation_cache: Optional[BaseCache] = None,
|
|
25
|
+
message_cache: Optional[BaseCache] = None
|
|
26
|
+
):
|
|
27
|
+
"""
|
|
28
|
+
Initialize cache manager.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
conversation_cache: Cache instance for conversations
|
|
32
|
+
message_cache: Cache instance for messages
|
|
33
|
+
"""
|
|
34
|
+
self.conversation_cache = conversation_cache or MemoryCache(
|
|
35
|
+
max_size=100, default_ttl=600.0 # 10 minutes default
|
|
36
|
+
)
|
|
37
|
+
self.message_cache = message_cache or MemoryCache(
|
|
38
|
+
max_size=500, default_ttl=300.0 # 5 minutes default
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Ensure caches implement required interface
|
|
42
|
+
self._validate_cache_interface(self.conversation_cache)
|
|
43
|
+
self._validate_cache_interface(self.message_cache)
|
|
44
|
+
|
|
45
|
+
def _validate_cache_interface(self, cache: BaseCache) -> None:
|
|
46
|
+
"""Validate that cache implements required interface."""
|
|
47
|
+
required_methods = ['get', 'set', 'delete', 'clear', 'exists', 'size', 'keys']
|
|
48
|
+
for method in required_methods:
|
|
49
|
+
if not hasattr(cache, method) or not callable(getattr(cache, method)):
|
|
50
|
+
raise TypeError(f"Cache must implement {method} method")
|
|
51
|
+
|
|
52
|
+
def _get_conversation_key(self, conversation_id: str) -> str:
|
|
53
|
+
"""Generate cache key for conversation."""
|
|
54
|
+
return f"conv:{conversation_id}"
|
|
55
|
+
|
|
56
|
+
def _get_messages_key(self, conversation_id: str) -> str:
|
|
57
|
+
"""Generate cache key for conversation messages."""
|
|
58
|
+
return f"msgs:{conversation_id}"
|
|
59
|
+
|
|
60
|
+
def cache_conversation(
|
|
61
|
+
self,
|
|
62
|
+
conversation: Conversation,
|
|
63
|
+
ttl: Optional[float] = None
|
|
64
|
+
) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Cache a conversation.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
conversation: The conversation to cache
|
|
70
|
+
ttl: Time to live in seconds, None for default
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
key = self._get_conversation_key(conversation.conversation_id)
|
|
74
|
+
self.conversation_cache.set(key, conversation, ttl=ttl)
|
|
75
|
+
logger.debug(f"Cached conversation {conversation.conversation_id}")
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error(f"Failed to cache conversation {conversation.conversation_id}: {e}")
|
|
78
|
+
|
|
79
|
+
def get_conversation(self, conversation_id: str) -> Optional[Conversation]:
|
|
80
|
+
"""
|
|
81
|
+
Get a conversation from cache.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
conversation_id: The conversation ID
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
The cached conversation or None if not found
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
key = self._get_conversation_key(conversation_id)
|
|
91
|
+
conversation = self.conversation_cache.get(key)
|
|
92
|
+
if conversation:
|
|
93
|
+
logger.debug(f"Cache hit for conversation {conversation_id}")
|
|
94
|
+
else:
|
|
95
|
+
logger.debug(f"Cache miss for conversation {conversation_id}")
|
|
96
|
+
return conversation
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.error(f"Failed to get conversation {conversation_id} from cache: {e}")
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
def cache_messages(
|
|
102
|
+
self,
|
|
103
|
+
conversation_id: str,
|
|
104
|
+
messages: List[ConversationMessage],
|
|
105
|
+
ttl: Optional[float] = None
|
|
106
|
+
) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Cache messages for a conversation.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
conversation_id: The conversation ID
|
|
112
|
+
messages: List of messages to cache
|
|
113
|
+
ttl: Time to live in seconds, None for default
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
key = self._get_messages_key(conversation_id)
|
|
117
|
+
self.message_cache.set(key, messages, ttl=ttl)
|
|
118
|
+
logger.debug(f"Cached {len(messages)} messages for conversation {conversation_id}")
|
|
119
|
+
except Exception as e:
|
|
120
|
+
logger.error(f"Failed to cache messages for conversation {conversation_id}: {e}")
|
|
121
|
+
|
|
122
|
+
def get_messages(self, conversation_id: str) -> Optional[List[ConversationMessage]]:
|
|
123
|
+
"""
|
|
124
|
+
Get messages from cache.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
conversation_id: The conversation ID
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
List of cached messages or None if not found
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
key = self._get_messages_key(conversation_id)
|
|
134
|
+
messages = self.message_cache.get(key)
|
|
135
|
+
if messages:
|
|
136
|
+
logger.debug(f"Cache hit for messages of conversation {conversation_id}")
|
|
137
|
+
else:
|
|
138
|
+
logger.debug(f"Cache miss for messages of conversation {conversation_id}")
|
|
139
|
+
return messages
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error(f"Failed to get messages for conversation {conversation_id} from cache: {e}")
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
def invalidate_conversation(self, conversation_id: str) -> bool:
|
|
145
|
+
"""
|
|
146
|
+
Invalidate cached conversation.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
conversation_id: The conversation ID
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
True if conversation was cached and removed, False otherwise
|
|
153
|
+
"""
|
|
154
|
+
try:
|
|
155
|
+
key = self._get_conversation_key(conversation_id)
|
|
156
|
+
result = self.conversation_cache.delete(key)
|
|
157
|
+
if result:
|
|
158
|
+
logger.debug(f"Invalidated conversation {conversation_id}")
|
|
159
|
+
return result
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.error(f"Failed to invalidate conversation {conversation_id}: {e}")
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
def invalidate_messages(self, conversation_id: str) -> bool:
|
|
165
|
+
"""
|
|
166
|
+
Invalidate cached messages.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
conversation_id: The conversation ID
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
True if messages were cached and removed, False otherwise
|
|
173
|
+
"""
|
|
174
|
+
try:
|
|
175
|
+
key = self._get_messages_key(conversation_id)
|
|
176
|
+
result = self.message_cache.delete(key)
|
|
177
|
+
if result:
|
|
178
|
+
logger.debug(f"Invalidated messages for conversation {conversation_id}")
|
|
179
|
+
return result
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error(f"Failed to invalidate messages for conversation {conversation_id}: {e}")
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
def invalidate_all(self, conversation_id: str) -> Dict[str, bool]:
|
|
185
|
+
"""
|
|
186
|
+
Invalidate all cached data for a conversation.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
conversation_id: The conversation ID
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Dictionary with invalidation results
|
|
193
|
+
"""
|
|
194
|
+
return {
|
|
195
|
+
"conversation": self.invalidate_conversation(conversation_id),
|
|
196
|
+
"messages": self.invalidate_messages(conversation_id)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
def warm_conversation_cache(
|
|
200
|
+
self,
|
|
201
|
+
data_loader: Callable[[], List[Conversation]]
|
|
202
|
+
) -> int:
|
|
203
|
+
"""
|
|
204
|
+
Warm conversation cache with data.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
data_loader: Function that returns conversations to cache
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Number of conversations cached
|
|
211
|
+
"""
|
|
212
|
+
try:
|
|
213
|
+
conversations = data_loader()
|
|
214
|
+
count = 0
|
|
215
|
+
|
|
216
|
+
for conversation in conversations:
|
|
217
|
+
self.cache_conversation(conversation)
|
|
218
|
+
count += 1
|
|
219
|
+
|
|
220
|
+
logger.info(f"Warmed conversation cache with {count} conversations")
|
|
221
|
+
return count
|
|
222
|
+
|
|
223
|
+
except Exception as e:
|
|
224
|
+
logger.error(f"Failed to warm conversation cache: {e}")
|
|
225
|
+
return 0
|
|
226
|
+
|
|
227
|
+
def cache_conversations(
|
|
228
|
+
self,
|
|
229
|
+
conversations: List[Conversation],
|
|
230
|
+
ttl: Optional[float] = None
|
|
231
|
+
) -> int:
|
|
232
|
+
"""
|
|
233
|
+
Cache multiple conversations.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
conversations: List of conversations to cache
|
|
237
|
+
ttl: Time to live in seconds, None for default
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Number of conversations successfully cached
|
|
241
|
+
"""
|
|
242
|
+
count = 0
|
|
243
|
+
for conversation in conversations:
|
|
244
|
+
try:
|
|
245
|
+
self.cache_conversation(conversation, ttl=ttl)
|
|
246
|
+
count += 1
|
|
247
|
+
except Exception as e:
|
|
248
|
+
logger.error(f"Failed to cache conversation {conversation.conversation_id}: {e}")
|
|
249
|
+
|
|
250
|
+
return count
|
|
251
|
+
|
|
252
|
+
def invalidate_conversations(self, conversation_ids: List[str]) -> Dict[str, bool]:
|
|
253
|
+
"""
|
|
254
|
+
Invalidate multiple conversations.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
conversation_ids: List of conversation IDs to invalidate
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Dictionary mapping conversation IDs to invalidation results
|
|
261
|
+
"""
|
|
262
|
+
results = {}
|
|
263
|
+
for conversation_id in conversation_ids:
|
|
264
|
+
results[conversation_id] = self.invalidate_conversation(conversation_id)
|
|
265
|
+
|
|
266
|
+
return results
|
|
267
|
+
|
|
268
|
+
def clear_all_caches(self) -> None:
|
|
269
|
+
"""Clear all caches."""
|
|
270
|
+
try:
|
|
271
|
+
self.conversation_cache.clear()
|
|
272
|
+
self.message_cache.clear()
|
|
273
|
+
logger.info("Cleared all caches")
|
|
274
|
+
except Exception as e:
|
|
275
|
+
logger.error(f"Failed to clear caches: {e}")
|
|
276
|
+
|
|
277
|
+
def get_cache_statistics(self) -> Dict[str, Any]:
|
|
278
|
+
"""
|
|
279
|
+
Get statistics for all caches.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Dictionary with cache statistics
|
|
283
|
+
"""
|
|
284
|
+
try:
|
|
285
|
+
stats = {
|
|
286
|
+
"conversation_cache": {
|
|
287
|
+
"size": self.conversation_cache.size(),
|
|
288
|
+
"max_size": getattr(self.conversation_cache, 'max_size', 'unknown')
|
|
289
|
+
},
|
|
290
|
+
"message_cache": {
|
|
291
|
+
"size": self.message_cache.size(),
|
|
292
|
+
"max_size": getattr(self.message_cache, 'max_size', 'unknown')
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# Add detailed stats if available
|
|
297
|
+
if hasattr(self.conversation_cache, 'get_statistics'):
|
|
298
|
+
stats["conversation_cache"].update(
|
|
299
|
+
self.conversation_cache.get_statistics()
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
if hasattr(self.message_cache, 'get_statistics'):
|
|
303
|
+
stats["message_cache"].update(
|
|
304
|
+
self.message_cache.get_statistics()
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return stats
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.error(f"Failed to get cache statistics: {e}")
|
|
311
|
+
return {
|
|
312
|
+
"conversation_cache": {"size": 0, "max_size": "unknown"},
|
|
313
|
+
"message_cache": {"size": 0, "max_size": "unknown"},
|
|
314
|
+
"error": str(e)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
def is_conversation_cached(self, conversation_id: str) -> bool:
|
|
318
|
+
"""
|
|
319
|
+
Check if a conversation is cached.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
conversation_id: The conversation ID
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
True if conversation is cached, False otherwise
|
|
326
|
+
"""
|
|
327
|
+
try:
|
|
328
|
+
key = self._get_conversation_key(conversation_id)
|
|
329
|
+
return self.conversation_cache.exists(key)
|
|
330
|
+
except Exception as e:
|
|
331
|
+
logger.error(f"Failed to check if conversation {conversation_id} is cached: {e}")
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
def is_messages_cached(self, conversation_id: str) -> bool:
|
|
335
|
+
"""
|
|
336
|
+
Check if messages are cached.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
conversation_id: The conversation ID
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
True if messages are cached, False otherwise
|
|
343
|
+
"""
|
|
344
|
+
try:
|
|
345
|
+
key = self._get_messages_key(conversation_id)
|
|
346
|
+
return self.message_cache.exists(key)
|
|
347
|
+
except Exception as e:
|
|
348
|
+
logger.error(f"Failed to check if messages for conversation {conversation_id} are cached: {e}")
|
|
349
|
+
return False
|
|
350
|
+
|
|
351
|
+
def get_cached_conversation_ids(self) -> List[str]:
|
|
352
|
+
"""
|
|
353
|
+
Get all cached conversation IDs.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
List of conversation IDs currently cached
|
|
357
|
+
"""
|
|
358
|
+
try:
|
|
359
|
+
keys = self.conversation_cache.keys()
|
|
360
|
+
# Extract conversation IDs from cache keys
|
|
361
|
+
conversation_ids = []
|
|
362
|
+
for key in keys:
|
|
363
|
+
if key.startswith("conv:"):
|
|
364
|
+
conversation_ids.append(key[5:]) # Remove "conv:" prefix
|
|
365
|
+
return conversation_ids
|
|
366
|
+
except Exception as e:
|
|
367
|
+
logger.error(f"Failed to get cached conversation IDs: {e}")
|
|
368
|
+
return []
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory-based cache implementation with LRU eviction and TTL support.
|
|
3
|
+
|
|
4
|
+
This module provides a thread-safe in-memory cache with configurable
|
|
5
|
+
size limits, TTL expiration, and LRU eviction policies.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
import threading
|
|
10
|
+
from collections import OrderedDict
|
|
11
|
+
from typing import Optional, Any, List, Dict
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from .base_cache import BaseCache
|
|
15
|
+
|
|
16
|
+
# Sentinel object to distinguish between "no ttl provided" and "ttl=None"
|
|
17
|
+
_TTL_NOT_PROVIDED = object()
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CacheEntry:
|
|
21
|
+
"""Cache entry with value and expiration time."""
|
|
22
|
+
value: Any
|
|
23
|
+
expires_at: Optional[float] = None
|
|
24
|
+
|
|
25
|
+
def is_expired(self) -> bool:
|
|
26
|
+
"""Check if this entry has expired."""
|
|
27
|
+
if self.expires_at is None:
|
|
28
|
+
return False
|
|
29
|
+
return time.time() > self.expires_at
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MemoryCache(BaseCache):
|
|
33
|
+
"""Thread-safe in-memory cache with LRU eviction and TTL support."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, max_size: int = 100, default_ttl: float = 300.0):
|
|
36
|
+
"""
|
|
37
|
+
Initialize memory cache.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
max_size: Maximum number of items to cache
|
|
41
|
+
default_ttl: Default TTL in seconds for cached items
|
|
42
|
+
"""
|
|
43
|
+
if max_size <= 0:
|
|
44
|
+
raise ValueError("max_size must be positive")
|
|
45
|
+
|
|
46
|
+
self.max_size = max_size
|
|
47
|
+
self.default_ttl = default_ttl
|
|
48
|
+
self._cache: OrderedDict[str, CacheEntry] = OrderedDict()
|
|
49
|
+
self._lock = threading.RLock()
|
|
50
|
+
|
|
51
|
+
# Optional statistics
|
|
52
|
+
self.hit_count = 0
|
|
53
|
+
self.miss_count = 0
|
|
54
|
+
|
|
55
|
+
def get(self, key: str) -> Optional[Any]:
|
|
56
|
+
"""
|
|
57
|
+
Get a value from the cache.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
key: The cache key
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The cached value or None if not found/expired
|
|
64
|
+
"""
|
|
65
|
+
with self._lock:
|
|
66
|
+
entry = self._cache.get(key)
|
|
67
|
+
|
|
68
|
+
if entry is None:
|
|
69
|
+
self.miss_count += 1
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
if entry.is_expired():
|
|
73
|
+
# Remove expired entry
|
|
74
|
+
del self._cache[key]
|
|
75
|
+
self.miss_count += 1
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# Move to end (most recently used)
|
|
79
|
+
self._cache.move_to_end(key)
|
|
80
|
+
self.hit_count += 1
|
|
81
|
+
return entry.value
|
|
82
|
+
|
|
83
|
+
def set(self, key: str, value: Any, ttl: Optional[float] = _TTL_NOT_PROVIDED) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Set a value in the cache.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
key: The cache key
|
|
89
|
+
value: The value to cache
|
|
90
|
+
ttl: Time to live in seconds, None for no expiration
|
|
91
|
+
"""
|
|
92
|
+
with self._lock:
|
|
93
|
+
# Calculate expiration time
|
|
94
|
+
expires_at = None
|
|
95
|
+
|
|
96
|
+
if ttl is _TTL_NOT_PROVIDED:
|
|
97
|
+
# No ttl provided, use default_ttl behavior
|
|
98
|
+
if self.default_ttl > 0:
|
|
99
|
+
expires_at = time.time() + self.default_ttl
|
|
100
|
+
elif ttl is None:
|
|
101
|
+
# Explicit None means permanent caching
|
|
102
|
+
expires_at = None
|
|
103
|
+
elif ttl > 0:
|
|
104
|
+
# Positive ttl value
|
|
105
|
+
expires_at = time.time() + ttl
|
|
106
|
+
# ttl <= 0 means permanent caching (expires_at stays None)
|
|
107
|
+
|
|
108
|
+
# Create cache entry
|
|
109
|
+
entry = CacheEntry(value=value, expires_at=expires_at)
|
|
110
|
+
|
|
111
|
+
# If key already exists, update it
|
|
112
|
+
if key in self._cache:
|
|
113
|
+
self._cache[key] = entry
|
|
114
|
+
self._cache.move_to_end(key)
|
|
115
|
+
else:
|
|
116
|
+
# Add new entry
|
|
117
|
+
self._cache[key] = entry
|
|
118
|
+
|
|
119
|
+
# Evict oldest items if over capacity
|
|
120
|
+
while len(self._cache) > self.max_size:
|
|
121
|
+
oldest_key = next(iter(self._cache))
|
|
122
|
+
del self._cache[oldest_key]
|
|
123
|
+
|
|
124
|
+
def delete(self, key: str) -> bool:
|
|
125
|
+
"""
|
|
126
|
+
Delete a value from the cache.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
key: The cache key
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
True if the key was deleted, False if it didn't exist
|
|
133
|
+
"""
|
|
134
|
+
with self._lock:
|
|
135
|
+
if key in self._cache:
|
|
136
|
+
del self._cache[key]
|
|
137
|
+
return True
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
def clear(self) -> None:
|
|
141
|
+
"""Clear all items from the cache."""
|
|
142
|
+
with self._lock:
|
|
143
|
+
self._cache.clear()
|
|
144
|
+
|
|
145
|
+
def exists(self, key: str) -> bool:
|
|
146
|
+
"""
|
|
147
|
+
Check if a key exists in the cache.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
key: The cache key
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
True if the key exists and is not expired, False otherwise
|
|
154
|
+
"""
|
|
155
|
+
with self._lock:
|
|
156
|
+
entry = self._cache.get(key)
|
|
157
|
+
|
|
158
|
+
if entry is None:
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
if entry.is_expired():
|
|
162
|
+
# Remove expired entry
|
|
163
|
+
del self._cache[key]
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
return True
|
|
167
|
+
|
|
168
|
+
def size(self) -> int:
|
|
169
|
+
"""
|
|
170
|
+
Get the current number of items in the cache.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
The number of items currently in the cache
|
|
174
|
+
"""
|
|
175
|
+
with self._lock:
|
|
176
|
+
# Clean up expired items
|
|
177
|
+
self._cleanup_expired()
|
|
178
|
+
return len(self._cache)
|
|
179
|
+
|
|
180
|
+
def keys(self) -> List[str]:
|
|
181
|
+
"""
|
|
182
|
+
Get all keys currently in the cache.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
List of keys currently in the cache
|
|
186
|
+
"""
|
|
187
|
+
with self._lock:
|
|
188
|
+
# Clean up expired items
|
|
189
|
+
self._cleanup_expired()
|
|
190
|
+
return list(self._cache.keys())
|
|
191
|
+
|
|
192
|
+
def _cleanup_expired(self) -> None:
|
|
193
|
+
"""Remove all expired entries from the cache."""
|
|
194
|
+
current_time = time.time()
|
|
195
|
+
expired_keys = []
|
|
196
|
+
|
|
197
|
+
for key, entry in self._cache.items():
|
|
198
|
+
if entry.expires_at is not None and current_time > entry.expires_at:
|
|
199
|
+
expired_keys.append(key)
|
|
200
|
+
|
|
201
|
+
for key in expired_keys:
|
|
202
|
+
del self._cache[key]
|
|
203
|
+
|
|
204
|
+
def get_statistics(self) -> Dict[str, Any]:
|
|
205
|
+
"""
|
|
206
|
+
Get cache statistics.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Dictionary with cache statistics
|
|
210
|
+
"""
|
|
211
|
+
with self._lock:
|
|
212
|
+
self._cleanup_expired()
|
|
213
|
+
|
|
214
|
+
total_requests = self.hit_count + self.miss_count
|
|
215
|
+
hit_rate = self.hit_count / total_requests if total_requests > 0 else 0.0
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
"size": len(self._cache),
|
|
219
|
+
"max_size": self.max_size,
|
|
220
|
+
"hit_count": self.hit_count,
|
|
221
|
+
"miss_count": self.miss_count,
|
|
222
|
+
"hit_rate": hit_rate,
|
|
223
|
+
"default_ttl": self.default_ttl
|
|
224
|
+
}
|