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,546 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Restore manager for conversation data.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to restore conversation data from backups,
|
|
5
|
+
including version management and data validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import json
|
|
10
|
+
import shutil
|
|
11
|
+
import threading
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from ..exceptions import RestoreError, ConversationManagerError, BackupError
|
|
17
|
+
from ..config import ConversationManagerConfig
|
|
18
|
+
from .backup_manager import BackupManager, BackupMetadata
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RestoreManager:
|
|
22
|
+
"""
|
|
23
|
+
Manages restore operations for conversation data.
|
|
24
|
+
|
|
25
|
+
Provides functionality to restore data from full and incremental backups,
|
|
26
|
+
with validation and version management.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, config: ConversationManagerConfig, backup_manager: BackupManager):
|
|
30
|
+
"""
|
|
31
|
+
Initialize restore manager.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
config: Configuration object containing restore settings
|
|
35
|
+
backup_manager: BackupManager instance for accessing backup metadata
|
|
36
|
+
"""
|
|
37
|
+
self.config = config
|
|
38
|
+
self.backup_manager = backup_manager
|
|
39
|
+
self.storage_path = Path(config.storage_path)
|
|
40
|
+
self.backup_path = self.storage_path / "backups"
|
|
41
|
+
self.temp_path = self.storage_path / "temp"
|
|
42
|
+
|
|
43
|
+
# Ensure directories exist
|
|
44
|
+
self.temp_path.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
|
|
46
|
+
# Thread lock for restore operations
|
|
47
|
+
self._restore_lock = threading.Lock()
|
|
48
|
+
|
|
49
|
+
def restore_conversation(
|
|
50
|
+
self,
|
|
51
|
+
conversation_id: str,
|
|
52
|
+
backup_id: str,
|
|
53
|
+
target_directory: Optional[str] = None
|
|
54
|
+
) -> bool:
|
|
55
|
+
"""
|
|
56
|
+
Restore a specific conversation from backup.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
conversation_id: ID of the conversation to restore
|
|
60
|
+
backup_id: ID of the backup to restore from
|
|
61
|
+
target_directory: Optional target directory for restoration
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
True if restoration was successful
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
RestoreError: If restoration fails
|
|
68
|
+
"""
|
|
69
|
+
with self._restore_lock:
|
|
70
|
+
try:
|
|
71
|
+
# Get backup metadata
|
|
72
|
+
metadata = self.backup_manager.get_backup_metadata(backup_id)
|
|
73
|
+
if metadata is None:
|
|
74
|
+
raise RestoreError(f"Backup {backup_id} not found")
|
|
75
|
+
|
|
76
|
+
# Verify backup integrity
|
|
77
|
+
if not self.backup_manager.verify_backup(backup_id):
|
|
78
|
+
raise RestoreError(f"Backup {backup_id} integrity check failed")
|
|
79
|
+
|
|
80
|
+
# Build complete backup chain for incremental backups
|
|
81
|
+
backup_chain = self._build_backup_chain(backup_id)
|
|
82
|
+
|
|
83
|
+
# Find conversation file in backup chain
|
|
84
|
+
conversation_data = self._find_conversation_in_backups(
|
|
85
|
+
conversation_id, backup_chain
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if conversation_data is None:
|
|
89
|
+
raise RestoreError(
|
|
90
|
+
f"Conversation {conversation_id} not found in backup {backup_id}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Determine target directory
|
|
94
|
+
if target_directory is None:
|
|
95
|
+
target_dir = self.storage_path / "conversations"
|
|
96
|
+
else:
|
|
97
|
+
target_dir = Path(target_directory)
|
|
98
|
+
|
|
99
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
|
|
101
|
+
# Create backup of existing conversation if it exists
|
|
102
|
+
existing_file = target_dir / f"{conversation_id}.json"
|
|
103
|
+
if existing_file.exists():
|
|
104
|
+
backup_file = existing_file.with_suffix(
|
|
105
|
+
f".backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
106
|
+
)
|
|
107
|
+
shutil.copy2(existing_file, backup_file)
|
|
108
|
+
|
|
109
|
+
# Write restored conversation data
|
|
110
|
+
with open(existing_file, 'w', encoding='utf-8') as f:
|
|
111
|
+
json.dump(conversation_data, f, indent=2, ensure_ascii=False)
|
|
112
|
+
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
raise RestoreError(f"Failed to restore conversation {conversation_id}: {str(e)}") from e
|
|
117
|
+
|
|
118
|
+
def restore_full_backup(
|
|
119
|
+
self,
|
|
120
|
+
backup_id: str,
|
|
121
|
+
target_directory: Optional[str] = None,
|
|
122
|
+
overwrite_existing: bool = False
|
|
123
|
+
) -> Dict[str, any]:
|
|
124
|
+
"""
|
|
125
|
+
Restore all data from a full backup.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
backup_id: ID of the backup to restore from
|
|
129
|
+
target_directory: Optional target directory for restoration
|
|
130
|
+
overwrite_existing: Whether to overwrite existing files
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Dictionary containing restore results
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
RestoreError: If restoration fails
|
|
137
|
+
"""
|
|
138
|
+
with self._restore_lock:
|
|
139
|
+
try:
|
|
140
|
+
# Get backup metadata
|
|
141
|
+
metadata = self.backup_manager.get_backup_metadata(backup_id)
|
|
142
|
+
if metadata is None:
|
|
143
|
+
raise RestoreError(f"Backup {backup_id} not found")
|
|
144
|
+
|
|
145
|
+
if metadata.backup_type != "full":
|
|
146
|
+
raise RestoreError(f"Backup {backup_id} is not a full backup")
|
|
147
|
+
|
|
148
|
+
# Verify backup integrity
|
|
149
|
+
if not self.backup_manager.verify_backup(backup_id):
|
|
150
|
+
raise RestoreError(f"Backup {backup_id} integrity check failed")
|
|
151
|
+
|
|
152
|
+
backup_dir = self.backup_path / backup_id
|
|
153
|
+
|
|
154
|
+
# Determine target directory
|
|
155
|
+
if target_directory is None:
|
|
156
|
+
target_dir = self.storage_path
|
|
157
|
+
else:
|
|
158
|
+
target_dir = Path(target_directory)
|
|
159
|
+
|
|
160
|
+
# Create timestamp for backup naming
|
|
161
|
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
162
|
+
|
|
163
|
+
# Backup existing data if not overwriting
|
|
164
|
+
existing_conversations_dir = target_dir / "conversations"
|
|
165
|
+
existing_index_dir = target_dir / "index"
|
|
166
|
+
|
|
167
|
+
backed_up_files = []
|
|
168
|
+
if not overwrite_existing:
|
|
169
|
+
if existing_conversations_dir.exists():
|
|
170
|
+
backup_conversations_dir = target_dir / f"conversations.backup_{timestamp}"
|
|
171
|
+
shutil.copytree(existing_conversations_dir, backup_conversations_dir)
|
|
172
|
+
backed_up_files.append(str(backup_conversations_dir))
|
|
173
|
+
|
|
174
|
+
if existing_index_dir.exists():
|
|
175
|
+
backup_index_dir = target_dir / f"index.backup_{timestamp}"
|
|
176
|
+
shutil.copytree(existing_index_dir, backup_index_dir)
|
|
177
|
+
backed_up_files.append(str(backup_index_dir))
|
|
178
|
+
|
|
179
|
+
# Restore conversations
|
|
180
|
+
restored_conversations = []
|
|
181
|
+
conversations_backup_dir = backup_dir
|
|
182
|
+
if conversations_backup_dir.exists():
|
|
183
|
+
target_conversations_dir = target_dir / "conversations"
|
|
184
|
+
target_conversations_dir.mkdir(parents=True, exist_ok=True)
|
|
185
|
+
|
|
186
|
+
for conv_file in conversations_backup_dir.glob("*.json"):
|
|
187
|
+
target_file = target_conversations_dir / conv_file.name
|
|
188
|
+
shutil.copy2(conv_file, target_file)
|
|
189
|
+
restored_conversations.append(conv_file.stem)
|
|
190
|
+
|
|
191
|
+
# Restore index files
|
|
192
|
+
restored_indices = []
|
|
193
|
+
index_backup_dir = backup_dir / "index"
|
|
194
|
+
if index_backup_dir.exists():
|
|
195
|
+
target_index_dir = target_dir / "index"
|
|
196
|
+
target_index_dir.mkdir(parents=True, exist_ok=True)
|
|
197
|
+
|
|
198
|
+
for index_file in index_backup_dir.glob("*.json"):
|
|
199
|
+
target_file = target_index_dir / index_file.name
|
|
200
|
+
shutil.copy2(index_file, target_file)
|
|
201
|
+
restored_indices.append(index_file.name)
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
"backup_id": backup_id,
|
|
205
|
+
"restore_timestamp": datetime.now().isoformat(),
|
|
206
|
+
"restored_conversations": restored_conversations,
|
|
207
|
+
"restored_indices": restored_indices,
|
|
208
|
+
"backed_up_files": backed_up_files,
|
|
209
|
+
"overwrite_existing": overwrite_existing
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
raise RestoreError(f"Failed to restore full backup {backup_id}: {str(e)}") from e
|
|
214
|
+
|
|
215
|
+
def restore_point_in_time(
|
|
216
|
+
self,
|
|
217
|
+
target_timestamp: float,
|
|
218
|
+
target_directory: Optional[str] = None
|
|
219
|
+
) -> Dict[str, any]:
|
|
220
|
+
"""
|
|
221
|
+
Restore data to a specific point in time.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
target_timestamp: Target timestamp to restore to
|
|
225
|
+
target_directory: Optional target directory for restoration
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Dictionary containing restore results
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
RestoreError: If restoration fails
|
|
232
|
+
"""
|
|
233
|
+
with self._restore_lock:
|
|
234
|
+
try:
|
|
235
|
+
# Find the best backup for the target timestamp
|
|
236
|
+
backup_id = self._find_best_backup_for_timestamp(target_timestamp)
|
|
237
|
+
if backup_id is None:
|
|
238
|
+
raise RestoreError(f"No suitable backup found for timestamp {target_timestamp}")
|
|
239
|
+
|
|
240
|
+
metadata = self.backup_manager.get_backup_metadata(backup_id)
|
|
241
|
+
|
|
242
|
+
if metadata.backup_type == "full":
|
|
243
|
+
return self.restore_full_backup(
|
|
244
|
+
backup_id,
|
|
245
|
+
target_directory=target_directory
|
|
246
|
+
)
|
|
247
|
+
else:
|
|
248
|
+
# For incremental backups, we need to apply the backup chain
|
|
249
|
+
return self._restore_incremental_chain(
|
|
250
|
+
backup_id,
|
|
251
|
+
target_directory=target_directory
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
raise RestoreError(f"Failed to restore to timestamp {target_timestamp}: {str(e)}") from e
|
|
256
|
+
|
|
257
|
+
def list_restorable_conversations(self, backup_id: str) -> List[str]:
|
|
258
|
+
"""
|
|
259
|
+
List conversations that can be restored from a backup.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
backup_id: ID of the backup to check
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
List of conversation IDs that can be restored
|
|
266
|
+
|
|
267
|
+
Raises:
|
|
268
|
+
RestoreError: If backup cannot be accessed
|
|
269
|
+
"""
|
|
270
|
+
try:
|
|
271
|
+
metadata = self.backup_manager.get_backup_metadata(backup_id)
|
|
272
|
+
if metadata is None:
|
|
273
|
+
raise RestoreError(f"Backup {backup_id} not found")
|
|
274
|
+
|
|
275
|
+
if metadata.backup_type == "full":
|
|
276
|
+
return metadata.conversation_ids or []
|
|
277
|
+
else:
|
|
278
|
+
# For incremental backups, get conversations from backup chain
|
|
279
|
+
backup_chain = self._build_backup_chain(backup_id)
|
|
280
|
+
all_conversations = set()
|
|
281
|
+
|
|
282
|
+
for chain_backup_id in backup_chain:
|
|
283
|
+
chain_metadata = self.backup_manager.get_backup_metadata(chain_backup_id)
|
|
284
|
+
if chain_metadata and chain_metadata.conversation_ids:
|
|
285
|
+
all_conversations.update(chain_metadata.conversation_ids)
|
|
286
|
+
|
|
287
|
+
return list(all_conversations)
|
|
288
|
+
|
|
289
|
+
except Exception as e:
|
|
290
|
+
raise RestoreError(f"Failed to list restorable conversations: {str(e)}") from e
|
|
291
|
+
|
|
292
|
+
def validate_backup_chain(self, backup_id: str) -> Dict[str, any]:
|
|
293
|
+
"""
|
|
294
|
+
Validate the integrity of a backup chain.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
backup_id: ID of the backup to validate
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Dictionary containing validation results
|
|
301
|
+
|
|
302
|
+
Raises:
|
|
303
|
+
RestoreError: If validation fails
|
|
304
|
+
"""
|
|
305
|
+
try:
|
|
306
|
+
backup_chain = self._build_backup_chain(backup_id)
|
|
307
|
+
validation_results = {
|
|
308
|
+
"backup_id": backup_id,
|
|
309
|
+
"chain_length": len(backup_chain),
|
|
310
|
+
"chain_backups": [],
|
|
311
|
+
"is_valid": True,
|
|
312
|
+
"errors": []
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for chain_backup_id in backup_chain:
|
|
316
|
+
metadata = self.backup_manager.get_backup_metadata(chain_backup_id)
|
|
317
|
+
if metadata is None:
|
|
318
|
+
validation_results["is_valid"] = False
|
|
319
|
+
validation_results["errors"].append(f"Backup {chain_backup_id} metadata not found")
|
|
320
|
+
continue
|
|
321
|
+
|
|
322
|
+
# Verify backup integrity
|
|
323
|
+
is_valid = self.backup_manager.verify_backup(chain_backup_id)
|
|
324
|
+
|
|
325
|
+
backup_info = {
|
|
326
|
+
"backup_id": chain_backup_id,
|
|
327
|
+
"backup_type": metadata.backup_type,
|
|
328
|
+
"timestamp": metadata.timestamp,
|
|
329
|
+
"is_valid": is_valid
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if not is_valid:
|
|
333
|
+
validation_results["is_valid"] = False
|
|
334
|
+
validation_results["errors"].append(f"Backup {chain_backup_id} integrity check failed")
|
|
335
|
+
|
|
336
|
+
validation_results["chain_backups"].append(backup_info)
|
|
337
|
+
|
|
338
|
+
return validation_results
|
|
339
|
+
|
|
340
|
+
except Exception as e:
|
|
341
|
+
raise RestoreError(f"Failed to validate backup chain: {str(e)}") from e
|
|
342
|
+
|
|
343
|
+
def get_restore_preview(self, backup_id: str) -> Dict[str, any]:
|
|
344
|
+
"""
|
|
345
|
+
Get a preview of what would be restored from a backup.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
backup_id: ID of the backup to preview
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Dictionary containing restore preview information
|
|
352
|
+
|
|
353
|
+
Raises:
|
|
354
|
+
RestoreError: If preview generation fails
|
|
355
|
+
"""
|
|
356
|
+
try:
|
|
357
|
+
metadata = self.backup_manager.get_backup_metadata(backup_id)
|
|
358
|
+
if metadata is None:
|
|
359
|
+
raise RestoreError(f"Backup {backup_id} not found")
|
|
360
|
+
|
|
361
|
+
backup_chain = self._build_backup_chain(backup_id)
|
|
362
|
+
|
|
363
|
+
preview = {
|
|
364
|
+
"backup_id": backup_id,
|
|
365
|
+
"backup_type": metadata.backup_type,
|
|
366
|
+
"backup_timestamp": metadata.timestamp,
|
|
367
|
+
"backup_chain": backup_chain,
|
|
368
|
+
"restorable_conversations": self.list_restorable_conversations(backup_id),
|
|
369
|
+
"total_file_count": 0,
|
|
370
|
+
"total_size_bytes": 0
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
# Calculate total size and file count for the chain
|
|
374
|
+
for chain_backup_id in backup_chain:
|
|
375
|
+
chain_metadata = self.backup_manager.get_backup_metadata(chain_backup_id)
|
|
376
|
+
if chain_metadata:
|
|
377
|
+
preview["total_file_count"] += chain_metadata.file_count
|
|
378
|
+
preview["total_size_bytes"] += chain_metadata.total_size_bytes
|
|
379
|
+
|
|
380
|
+
return preview
|
|
381
|
+
|
|
382
|
+
except Exception as e:
|
|
383
|
+
raise RestoreError(f"Failed to generate restore preview: {str(e)}") from e
|
|
384
|
+
|
|
385
|
+
def _build_backup_chain(self, backup_id: str) -> List[str]:
|
|
386
|
+
"""
|
|
387
|
+
Build the complete backup chain for a backup.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
backup_id: ID of the backup to build chain for
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
List of backup IDs in the chain, ordered from base to target
|
|
394
|
+
"""
|
|
395
|
+
chain = []
|
|
396
|
+
current_backup_id = backup_id
|
|
397
|
+
|
|
398
|
+
while current_backup_id:
|
|
399
|
+
metadata = self.backup_manager.get_backup_metadata(current_backup_id)
|
|
400
|
+
if metadata is None:
|
|
401
|
+
break
|
|
402
|
+
|
|
403
|
+
chain.insert(0, current_backup_id) # Insert at beginning
|
|
404
|
+
|
|
405
|
+
if metadata.backup_type == "full":
|
|
406
|
+
break # Reached the base backup
|
|
407
|
+
|
|
408
|
+
current_backup_id = metadata.base_backup_id
|
|
409
|
+
|
|
410
|
+
return chain
|
|
411
|
+
|
|
412
|
+
def _find_conversation_in_backups(
|
|
413
|
+
self,
|
|
414
|
+
conversation_id: str,
|
|
415
|
+
backup_chain: List[str]
|
|
416
|
+
) -> Optional[Dict]:
|
|
417
|
+
"""
|
|
418
|
+
Find conversation data in backup chain.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
conversation_id: ID of the conversation to find
|
|
422
|
+
backup_chain: List of backup IDs to search in
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
Conversation data or None if not found
|
|
426
|
+
"""
|
|
427
|
+
# Search from newest to oldest backup
|
|
428
|
+
for backup_id in reversed(backup_chain):
|
|
429
|
+
backup_dir = self.backup_path / backup_id
|
|
430
|
+
conv_file = backup_dir / f"{conversation_id}.json"
|
|
431
|
+
|
|
432
|
+
if conv_file.exists():
|
|
433
|
+
try:
|
|
434
|
+
with open(conv_file, 'r', encoding='utf-8') as f:
|
|
435
|
+
return json.load(f)
|
|
436
|
+
except Exception:
|
|
437
|
+
continue # Try next backup in chain
|
|
438
|
+
|
|
439
|
+
return None
|
|
440
|
+
|
|
441
|
+
def _find_best_backup_for_timestamp(self, target_timestamp: float) -> Optional[str]:
|
|
442
|
+
"""
|
|
443
|
+
Find the best backup for a target timestamp.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
target_timestamp: Target timestamp
|
|
447
|
+
|
|
448
|
+
Returns:
|
|
449
|
+
Backup ID or None if no suitable backup found
|
|
450
|
+
"""
|
|
451
|
+
backups = self.backup_manager.list_backups()
|
|
452
|
+
|
|
453
|
+
# Find backups that are before or at the target timestamp
|
|
454
|
+
suitable_backups = [
|
|
455
|
+
b for b in backups
|
|
456
|
+
if b.timestamp <= target_timestamp
|
|
457
|
+
]
|
|
458
|
+
|
|
459
|
+
if not suitable_backups:
|
|
460
|
+
return None
|
|
461
|
+
|
|
462
|
+
# Return the most recent suitable backup
|
|
463
|
+
best_backup = max(suitable_backups, key=lambda x: x.timestamp)
|
|
464
|
+
return best_backup.backup_id
|
|
465
|
+
|
|
466
|
+
def _restore_incremental_chain(
|
|
467
|
+
self,
|
|
468
|
+
backup_id: str,
|
|
469
|
+
target_directory: Optional[str] = None
|
|
470
|
+
) -> Dict[str, any]:
|
|
471
|
+
"""
|
|
472
|
+
Restore from an incremental backup chain.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
backup_id: ID of the incremental backup
|
|
476
|
+
target_directory: Optional target directory
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
Dictionary containing restore results
|
|
480
|
+
"""
|
|
481
|
+
backup_chain = self._build_backup_chain(backup_id)
|
|
482
|
+
|
|
483
|
+
if not backup_chain:
|
|
484
|
+
raise RestoreError(f"Empty backup chain for {backup_id}")
|
|
485
|
+
|
|
486
|
+
# Start with full backup restoration
|
|
487
|
+
full_backup_id = backup_chain[0]
|
|
488
|
+
results = self.restore_full_backup(
|
|
489
|
+
full_backup_id,
|
|
490
|
+
target_directory=target_directory,
|
|
491
|
+
overwrite_existing=True
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
# Apply incremental backups in order
|
|
495
|
+
for incremental_backup_id in backup_chain[1:]:
|
|
496
|
+
incremental_results = self._apply_incremental_backup(
|
|
497
|
+
incremental_backup_id,
|
|
498
|
+
target_directory or str(self.storage_path)
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Merge results
|
|
502
|
+
results["restored_conversations"].extend(
|
|
503
|
+
incremental_results.get("restored_conversations", [])
|
|
504
|
+
)
|
|
505
|
+
results["restored_indices"].extend(
|
|
506
|
+
incremental_results.get("restored_indices", [])
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Remove duplicates
|
|
510
|
+
results["restored_conversations"] = list(set(results["restored_conversations"]))
|
|
511
|
+
results["restored_indices"] = list(set(results["restored_indices"]))
|
|
512
|
+
|
|
513
|
+
return results
|
|
514
|
+
|
|
515
|
+
def _apply_incremental_backup(
|
|
516
|
+
self,
|
|
517
|
+
backup_id: str,
|
|
518
|
+
target_directory: str
|
|
519
|
+
) -> Dict[str, any]:
|
|
520
|
+
"""Apply an incremental backup to the target directory."""
|
|
521
|
+
backup_dir = self.backup_path / backup_id
|
|
522
|
+
target_dir = Path(target_directory)
|
|
523
|
+
|
|
524
|
+
restored_conversations = []
|
|
525
|
+
restored_indices = []
|
|
526
|
+
|
|
527
|
+
# Apply conversation files
|
|
528
|
+
for conv_file in backup_dir.glob("*.json"):
|
|
529
|
+
target_file = target_dir / "conversations" / conv_file.name
|
|
530
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
531
|
+
shutil.copy2(conv_file, target_file)
|
|
532
|
+
restored_conversations.append(conv_file.stem)
|
|
533
|
+
|
|
534
|
+
# Apply index files
|
|
535
|
+
index_backup_dir = backup_dir / "index"
|
|
536
|
+
if index_backup_dir.exists():
|
|
537
|
+
for index_file in index_backup_dir.glob("*.json"):
|
|
538
|
+
target_file = target_dir / "index" / index_file.name
|
|
539
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
540
|
+
shutil.copy2(index_file, target_file)
|
|
541
|
+
restored_indices.append(index_file.name)
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
"restored_conversations": restored_conversations,
|
|
545
|
+
"restored_indices": restored_indices
|
|
546
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cache module for conversation management.
|
|
3
|
+
|
|
4
|
+
This module provides caching functionality for conversations and messages,
|
|
5
|
+
including memory-based caching with LRU eviction and TTL support.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .base_cache import BaseCache
|
|
9
|
+
from .memory_cache import MemoryCache
|
|
10
|
+
from .cache_manager import CacheManager
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'BaseCache',
|
|
14
|
+
'MemoryCache',
|
|
15
|
+
'CacheManager'
|
|
16
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base cache interface for conversation caching.
|
|
3
|
+
|
|
4
|
+
This module defines the abstract base class for all cache implementations,
|
|
5
|
+
providing a consistent interface for caching operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import Optional, Any, List
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseCache(ABC):
|
|
13
|
+
"""Abstract base class for cache implementations."""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def get(self, key: str) -> Optional[Any]:
|
|
17
|
+
"""
|
|
18
|
+
Get a value from the cache.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
key: The cache key
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The cached value or None if not found/expired
|
|
25
|
+
"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Set a value in the cache.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
key: The cache key
|
|
35
|
+
value: The value to cache
|
|
36
|
+
ttl: Time to live in seconds, None for no expiration
|
|
37
|
+
"""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def delete(self, key: str) -> bool:
|
|
42
|
+
"""
|
|
43
|
+
Delete a value from the cache.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
key: The cache key
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
True if the key was deleted, False if it didn't exist
|
|
50
|
+
"""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def clear(self) -> None:
|
|
55
|
+
"""Clear all items from the cache."""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def exists(self, key: str) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Check if a key exists in the cache.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
key: The cache key
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
True if the key exists and is not expired, False otherwise
|
|
68
|
+
"""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def size(self) -> int:
|
|
73
|
+
"""
|
|
74
|
+
Get the current number of items in the cache.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The number of items currently in the cache
|
|
78
|
+
"""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def keys(self) -> List[str]:
|
|
83
|
+
"""
|
|
84
|
+
Get all keys currently in the cache.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
List of keys currently in the cache
|
|
88
|
+
"""
|
|
89
|
+
pass
|