vector-inspector 0.2.0__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.
- vector_inspector/__init__.py +3 -0
- vector_inspector/__main__.py +4 -0
- vector_inspector/core/__init__.py +1 -0
- vector_inspector/core/connections/__init__.py +7 -0
- vector_inspector/core/connections/base_connection.py +233 -0
- vector_inspector/core/connections/chroma_connection.py +384 -0
- vector_inspector/core/connections/qdrant_connection.py +723 -0
- vector_inspector/core/connections/template_connection.py +346 -0
- vector_inspector/main.py +21 -0
- vector_inspector/services/__init__.py +1 -0
- vector_inspector/services/backup_restore_service.py +286 -0
- vector_inspector/services/filter_service.py +72 -0
- vector_inspector/services/import_export_service.py +287 -0
- vector_inspector/services/settings_service.py +60 -0
- vector_inspector/services/visualization_service.py +116 -0
- vector_inspector/ui/__init__.py +1 -0
- vector_inspector/ui/components/__init__.py +1 -0
- vector_inspector/ui/components/backup_restore_dialog.py +350 -0
- vector_inspector/ui/components/filter_builder.py +370 -0
- vector_inspector/ui/components/item_dialog.py +118 -0
- vector_inspector/ui/components/loading_dialog.py +30 -0
- vector_inspector/ui/main_window.py +288 -0
- vector_inspector/ui/views/__init__.py +1 -0
- vector_inspector/ui/views/collection_browser.py +112 -0
- vector_inspector/ui/views/connection_view.py +423 -0
- vector_inspector/ui/views/metadata_view.py +555 -0
- vector_inspector/ui/views/search_view.py +268 -0
- vector_inspector/ui/views/visualization_view.py +245 -0
- vector_inspector-0.2.0.dist-info/METADATA +382 -0
- vector_inspector-0.2.0.dist-info/RECORD +32 -0
- vector_inspector-0.2.0.dist-info/WHEEL +4 -0
- vector_inspector-0.2.0.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""Dialog for backup and restore operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from PySide6.QtWidgets import (
|
|
5
|
+
QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget,
|
|
6
|
+
QPushButton, QLabel, QListWidget, QListWidgetItem, QFileDialog,
|
|
7
|
+
QMessageBox, QCheckBox, QLineEdit, QGroupBox, QFormLayout, QApplication
|
|
8
|
+
)
|
|
9
|
+
from PySide6.QtCore import Qt
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from vector_inspector.core.connections.base_connection import VectorDBConnection
|
|
13
|
+
from vector_inspector.services.backup_restore_service import BackupRestoreService
|
|
14
|
+
from vector_inspector.services.settings_service import SettingsService
|
|
15
|
+
from vector_inspector.ui.components.loading_dialog import LoadingDialog
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BackupRestoreDialog(QDialog):
|
|
19
|
+
"""Dialog for managing backups and restores."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, connection: VectorDBConnection, collection_name: str = "", parent=None):
|
|
22
|
+
super().__init__(parent)
|
|
23
|
+
self.connection = connection
|
|
24
|
+
self.collection_name = collection_name
|
|
25
|
+
self.backup_service = BackupRestoreService()
|
|
26
|
+
self.settings_service = SettingsService()
|
|
27
|
+
|
|
28
|
+
# Load backup directory from settings or use default
|
|
29
|
+
default_backup_dir = str(Path.home() / "vector-viewer-backups")
|
|
30
|
+
self.backup_dir = self.settings_service.get("backup_directory", default_backup_dir)
|
|
31
|
+
|
|
32
|
+
self.loading_dialog = LoadingDialog("Processing...", self)
|
|
33
|
+
|
|
34
|
+
self.setWindowTitle("Backup & Restore")
|
|
35
|
+
self.setMinimumSize(600, 500)
|
|
36
|
+
|
|
37
|
+
self._setup_ui()
|
|
38
|
+
self._refresh_backups_list()
|
|
39
|
+
|
|
40
|
+
def _setup_ui(self):
|
|
41
|
+
"""Setup dialog UI."""
|
|
42
|
+
layout = QVBoxLayout(self)
|
|
43
|
+
|
|
44
|
+
# Tabs for backup and restore
|
|
45
|
+
tabs = QTabWidget()
|
|
46
|
+
|
|
47
|
+
# Backup tab
|
|
48
|
+
backup_tab = self._create_backup_tab()
|
|
49
|
+
tabs.addTab(backup_tab, "Create Backup")
|
|
50
|
+
|
|
51
|
+
# Restore tab
|
|
52
|
+
restore_tab = self._create_restore_tab()
|
|
53
|
+
tabs.addTab(restore_tab, "Restore from Backup")
|
|
54
|
+
|
|
55
|
+
layout.addWidget(tabs)
|
|
56
|
+
|
|
57
|
+
# Close button
|
|
58
|
+
close_button = QPushButton("Close")
|
|
59
|
+
close_button.clicked.connect(self.accept)
|
|
60
|
+
layout.addWidget(close_button, alignment=Qt.AlignRight)
|
|
61
|
+
|
|
62
|
+
def _create_backup_tab(self) -> QWidget:
|
|
63
|
+
"""Create the backup tab."""
|
|
64
|
+
widget = QWidget()
|
|
65
|
+
layout = QVBoxLayout(widget)
|
|
66
|
+
|
|
67
|
+
# Collection selection
|
|
68
|
+
collection_group = QGroupBox("Backup Configuration")
|
|
69
|
+
collection_layout = QFormLayout()
|
|
70
|
+
|
|
71
|
+
# Collection name
|
|
72
|
+
collection_layout.addRow("Collection:", QLabel(self.collection_name or "No collection selected"))
|
|
73
|
+
|
|
74
|
+
# Backup directory
|
|
75
|
+
dir_layout = QHBoxLayout()
|
|
76
|
+
self.backup_dir_input = QLineEdit(self.backup_dir)
|
|
77
|
+
self.backup_dir_input.setReadOnly(True)
|
|
78
|
+
dir_layout.addWidget(self.backup_dir_input)
|
|
79
|
+
|
|
80
|
+
browse_btn = QPushButton("Browse...")
|
|
81
|
+
browse_btn.clicked.connect(self._select_backup_dir)
|
|
82
|
+
dir_layout.addWidget(browse_btn)
|
|
83
|
+
|
|
84
|
+
collection_layout.addRow("Backup Directory:", dir_layout)
|
|
85
|
+
|
|
86
|
+
# Options
|
|
87
|
+
self.include_embeddings_check = QCheckBox("Include embedding vectors (larger file size)")
|
|
88
|
+
self.include_embeddings_check.setChecked(True)
|
|
89
|
+
collection_layout.addRow("Options:", self.include_embeddings_check)
|
|
90
|
+
|
|
91
|
+
collection_group.setLayout(collection_layout)
|
|
92
|
+
layout.addWidget(collection_group)
|
|
93
|
+
|
|
94
|
+
# Info label
|
|
95
|
+
info_label = QLabel(
|
|
96
|
+
"Backup will create a compressed archive containing all collection data, "
|
|
97
|
+
"metadata, and optionally embedding vectors."
|
|
98
|
+
)
|
|
99
|
+
info_label.setWordWrap(True)
|
|
100
|
+
info_label.setStyleSheet("color: gray; font-style: italic;")
|
|
101
|
+
layout.addWidget(info_label)
|
|
102
|
+
|
|
103
|
+
layout.addStretch()
|
|
104
|
+
|
|
105
|
+
# Create backup button
|
|
106
|
+
backup_button = QPushButton("Create Backup")
|
|
107
|
+
backup_button.clicked.connect(self._create_backup)
|
|
108
|
+
backup_button.setStyleSheet("QPushButton { font-weight: bold; padding: 8px; }")
|
|
109
|
+
layout.addWidget(backup_button)
|
|
110
|
+
|
|
111
|
+
return widget
|
|
112
|
+
|
|
113
|
+
def _create_restore_tab(self) -> QWidget:
|
|
114
|
+
"""Create the restore tab."""
|
|
115
|
+
widget = QWidget()
|
|
116
|
+
layout = QVBoxLayout(widget)
|
|
117
|
+
|
|
118
|
+
# Backup list
|
|
119
|
+
layout.addWidget(QLabel("Available Backups:"))
|
|
120
|
+
|
|
121
|
+
self.backups_list = QListWidget()
|
|
122
|
+
self.backups_list.itemSelectionChanged.connect(self._on_backup_selected)
|
|
123
|
+
layout.addWidget(self.backups_list)
|
|
124
|
+
|
|
125
|
+
# Refresh button
|
|
126
|
+
refresh_btn = QPushButton("Refresh List")
|
|
127
|
+
refresh_btn.clicked.connect(self._refresh_backups_list)
|
|
128
|
+
layout.addWidget(refresh_btn)
|
|
129
|
+
|
|
130
|
+
# Restore options
|
|
131
|
+
options_group = QGroupBox("Restore Options")
|
|
132
|
+
options_layout = QVBoxLayout()
|
|
133
|
+
|
|
134
|
+
# New collection name
|
|
135
|
+
name_layout = QHBoxLayout()
|
|
136
|
+
name_layout.addWidget(QLabel("Restore as:"))
|
|
137
|
+
self.restore_name_input = QLineEdit()
|
|
138
|
+
self.restore_name_input.setPlaceholderText("Leave empty to use original name")
|
|
139
|
+
name_layout.addWidget(self.restore_name_input)
|
|
140
|
+
options_layout.addLayout(name_layout)
|
|
141
|
+
|
|
142
|
+
# Overwrite checkbox
|
|
143
|
+
self.overwrite_check = QCheckBox("Overwrite if collection exists")
|
|
144
|
+
self.overwrite_check.setChecked(False)
|
|
145
|
+
options_layout.addWidget(self.overwrite_check)
|
|
146
|
+
|
|
147
|
+
options_group.setLayout(options_layout)
|
|
148
|
+
layout.addWidget(options_group)
|
|
149
|
+
|
|
150
|
+
# Restore and delete buttons
|
|
151
|
+
button_layout = QHBoxLayout()
|
|
152
|
+
|
|
153
|
+
self.restore_button = QPushButton("Restore Selected")
|
|
154
|
+
self.restore_button.clicked.connect(self._restore_backup)
|
|
155
|
+
self.restore_button.setEnabled(False)
|
|
156
|
+
self.restore_button.setStyleSheet("QPushButton { font-weight: bold; padding: 8px; }")
|
|
157
|
+
button_layout.addWidget(self.restore_button)
|
|
158
|
+
|
|
159
|
+
self.delete_backup_button = QPushButton("Delete Selected")
|
|
160
|
+
self.delete_backup_button.clicked.connect(self._delete_backup)
|
|
161
|
+
self.delete_backup_button.setEnabled(False)
|
|
162
|
+
button_layout.addWidget(self.delete_backup_button)
|
|
163
|
+
|
|
164
|
+
layout.addLayout(button_layout)
|
|
165
|
+
|
|
166
|
+
return widget
|
|
167
|
+
|
|
168
|
+
def _select_backup_dir(self):
|
|
169
|
+
"""Select backup directory."""
|
|
170
|
+
dir_path = QFileDialog.getExistingDirectory(
|
|
171
|
+
self,
|
|
172
|
+
"Select Backup Directory",
|
|
173
|
+
self.backup_dir
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if dir_path:
|
|
177
|
+
self.backup_dir = dir_path
|
|
178
|
+
self.backup_dir_input.setText(dir_path)
|
|
179
|
+
|
|
180
|
+
# Save to settings
|
|
181
|
+
self.settings_service.set("backup_directory", dir_path)
|
|
182
|
+
|
|
183
|
+
self._refresh_backups_list()
|
|
184
|
+
|
|
185
|
+
def _create_backup(self):
|
|
186
|
+
"""Create a backup of the current collection."""
|
|
187
|
+
if not self.collection_name:
|
|
188
|
+
QMessageBox.warning(self, "No Collection", "No collection selected for backup.")
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
# Create backup
|
|
192
|
+
include_embeddings = self.include_embeddings_check.isChecked()
|
|
193
|
+
|
|
194
|
+
self.loading_dialog.show_loading("Creating backup...")
|
|
195
|
+
QApplication.processEvents()
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
backup_path = self.backup_service.backup_collection(
|
|
199
|
+
self.connection,
|
|
200
|
+
self.collection_name,
|
|
201
|
+
self.backup_dir,
|
|
202
|
+
include_embeddings=include_embeddings
|
|
203
|
+
)
|
|
204
|
+
finally:
|
|
205
|
+
self.loading_dialog.hide_loading()
|
|
206
|
+
|
|
207
|
+
if backup_path:
|
|
208
|
+
QMessageBox.information(
|
|
209
|
+
self,
|
|
210
|
+
"Backup Successful",
|
|
211
|
+
f"Backup created successfully:\n{backup_path}"
|
|
212
|
+
)
|
|
213
|
+
self._refresh_backups_list()
|
|
214
|
+
else:
|
|
215
|
+
QMessageBox.warning(self, "Backup Failed", "Failed to create backup.")
|
|
216
|
+
|
|
217
|
+
def _refresh_backups_list(self):
|
|
218
|
+
"""Refresh the list of available backups."""
|
|
219
|
+
self.backups_list.clear()
|
|
220
|
+
|
|
221
|
+
backups = self.backup_service.list_backups(self.backup_dir)
|
|
222
|
+
|
|
223
|
+
for backup in backups:
|
|
224
|
+
# Format file size
|
|
225
|
+
size_mb = backup["file_size"] / (1024 * 1024)
|
|
226
|
+
|
|
227
|
+
item_text = (
|
|
228
|
+
f"{backup['collection_name']} - {backup['timestamp']}\n"
|
|
229
|
+
f" Items: {backup['item_count']}, Size: {size_mb:.2f} MB\n"
|
|
230
|
+
f" File: {backup['file_name']}"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
item = QListWidgetItem(item_text)
|
|
234
|
+
item.setData(Qt.UserRole, backup["file_path"])
|
|
235
|
+
self.backups_list.addItem(item)
|
|
236
|
+
|
|
237
|
+
if not backups:
|
|
238
|
+
item = QListWidgetItem("No backups found in directory")
|
|
239
|
+
item.setFlags(Qt.NoItemFlags)
|
|
240
|
+
self.backups_list.addItem(item)
|
|
241
|
+
|
|
242
|
+
def _on_backup_selected(self):
|
|
243
|
+
"""Handle backup selection."""
|
|
244
|
+
has_selection = len(self.backups_list.selectedItems()) > 0
|
|
245
|
+
self.restore_button.setEnabled(has_selection)
|
|
246
|
+
self.delete_backup_button.setEnabled(has_selection)
|
|
247
|
+
|
|
248
|
+
def _restore_backup(self):
|
|
249
|
+
"""Restore a backup."""
|
|
250
|
+
selected_items = self.backups_list.selectedItems()
|
|
251
|
+
if not selected_items:
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
backup_file = selected_items[0].data(Qt.UserRole)
|
|
255
|
+
if not backup_file:
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
# Read backup metadata to get original collection name
|
|
259
|
+
try:
|
|
260
|
+
import zipfile
|
|
261
|
+
import json
|
|
262
|
+
with zipfile.ZipFile(backup_file, 'r') as zipf:
|
|
263
|
+
metadata_str = zipf.read('metadata.json').decode('utf-8')
|
|
264
|
+
metadata = json.loads(metadata_str)
|
|
265
|
+
original_name = metadata.get("collection_name", "unknown")
|
|
266
|
+
except Exception as e:
|
|
267
|
+
QMessageBox.warning(self, "Error", f"Failed to read backup metadata: {e}")
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
# Get restore options
|
|
271
|
+
restore_name = self.restore_name_input.text().strip()
|
|
272
|
+
overwrite = self.overwrite_check.isChecked()
|
|
273
|
+
|
|
274
|
+
# Determine final collection name
|
|
275
|
+
final_name = restore_name if restore_name else original_name
|
|
276
|
+
|
|
277
|
+
# Build confirmation message
|
|
278
|
+
if restore_name:
|
|
279
|
+
msg = f"Restore backup to NEW collection: '{final_name}'"
|
|
280
|
+
else:
|
|
281
|
+
msg = f"Restore backup to ORIGINAL collection: '{final_name}'"
|
|
282
|
+
|
|
283
|
+
# Check if collection exists
|
|
284
|
+
existing_collections = self.connection.list_collections()
|
|
285
|
+
if final_name in existing_collections:
|
|
286
|
+
if overwrite:
|
|
287
|
+
msg += f"\n\n⚠️ WARNING: This will DELETE and replace the existing collection '{final_name}'!"
|
|
288
|
+
else:
|
|
289
|
+
QMessageBox.warning(
|
|
290
|
+
self,
|
|
291
|
+
"Collection Exists",
|
|
292
|
+
f"Collection '{final_name}' already exists.\n\n"
|
|
293
|
+
f"Check 'Overwrite existing collection' to replace it, or enter a different name."
|
|
294
|
+
)
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
# Confirm
|
|
298
|
+
reply = QMessageBox.question(
|
|
299
|
+
self,
|
|
300
|
+
"Confirm Restore",
|
|
301
|
+
msg,
|
|
302
|
+
QMessageBox.Yes | QMessageBox.No
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
if reply != QMessageBox.Yes:
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
self.loading_dialog.show_loading("Restoring backup...")
|
|
309
|
+
QApplication.processEvents()
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
# Restore
|
|
313
|
+
success = self.backup_service.restore_collection(
|
|
314
|
+
self.connection,
|
|
315
|
+
backup_file,
|
|
316
|
+
collection_name=restore_name if restore_name else None,
|
|
317
|
+
overwrite=overwrite
|
|
318
|
+
)
|
|
319
|
+
finally:
|
|
320
|
+
self.loading_dialog.hide_loading()
|
|
321
|
+
|
|
322
|
+
if success:
|
|
323
|
+
QMessageBox.information(self, "Restore Successful", f"Backup restored successfully to collection '{final_name}'.")
|
|
324
|
+
else:
|
|
325
|
+
QMessageBox.warning(self, "Restore Failed", "Failed to restore backup.")
|
|
326
|
+
|
|
327
|
+
def _delete_backup(self):
|
|
328
|
+
"""Delete a backup file."""
|
|
329
|
+
selected_items = self.backups_list.selectedItems()
|
|
330
|
+
if not selected_items:
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
backup_file = selected_items[0].data(Qt.UserRole)
|
|
334
|
+
if not backup_file:
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
# Confirm deletion
|
|
338
|
+
reply = QMessageBox.question(
|
|
339
|
+
self,
|
|
340
|
+
"Confirm Deletion",
|
|
341
|
+
f"Delete this backup file?\n{Path(backup_file).name}",
|
|
342
|
+
QMessageBox.Yes | QMessageBox.No
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
if reply == QMessageBox.Yes:
|
|
346
|
+
if self.backup_service.delete_backup(backup_file):
|
|
347
|
+
QMessageBox.information(self, "Deleted", "Backup deleted successfully.")
|
|
348
|
+
self._refresh_backups_list()
|
|
349
|
+
else:
|
|
350
|
+
QMessageBox.warning(self, "Delete Failed", "Failed to delete backup.")
|