vector-inspector 0.3.9__py3-none-any.whl → 0.3.12__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.
Files changed (33) hide show
  1. vector_inspector/__init__.py +10 -1
  2. vector_inspector/core/connection_manager.py +91 -19
  3. vector_inspector/core/connections/base_connection.py +43 -43
  4. vector_inspector/core/connections/chroma_connection.py +1 -1
  5. vector_inspector/core/connections/pgvector_connection.py +12 -172
  6. vector_inspector/core/connections/pinecone_connection.py +596 -99
  7. vector_inspector/core/connections/qdrant_connection.py +35 -44
  8. vector_inspector/core/embedding_utils.py +14 -5
  9. vector_inspector/core/logging.py +3 -1
  10. vector_inspector/extensions/__init__.py +6 -0
  11. vector_inspector/extensions/telemetry_settings_panel.py +25 -0
  12. vector_inspector/main.py +45 -2
  13. vector_inspector/services/backup_restore_service.py +228 -15
  14. vector_inspector/services/settings_service.py +79 -19
  15. vector_inspector/services/telemetry_service.py +88 -0
  16. vector_inspector/ui/components/backup_restore_dialog.py +215 -101
  17. vector_inspector/ui/components/connection_manager_panel.py +155 -14
  18. vector_inspector/ui/dialogs/cross_db_migration.py +126 -99
  19. vector_inspector/ui/dialogs/settings_dialog.py +13 -6
  20. vector_inspector/ui/loading_screen.py +169 -0
  21. vector_inspector/ui/main_window.py +44 -19
  22. vector_inspector/ui/services/dialog_service.py +1 -0
  23. vector_inspector/ui/views/collection_browser.py +36 -34
  24. vector_inspector/ui/views/connection_view.py +7 -1
  25. vector_inspector/ui/views/info_panel.py +118 -52
  26. vector_inspector/ui/views/metadata_view.py +30 -31
  27. vector_inspector/ui/views/search_view.py +20 -19
  28. vector_inspector/ui/views/visualization_view.py +18 -15
  29. {vector_inspector-0.3.9.dist-info → vector_inspector-0.3.12.dist-info}/METADATA +19 -37
  30. {vector_inspector-0.3.9.dist-info → vector_inspector-0.3.12.dist-info}/RECORD +33 -29
  31. {vector_inspector-0.3.9.dist-info → vector_inspector-0.3.12.dist-info}/WHEEL +1 -1
  32. vector_inspector-0.3.12.dist-info/licenses/LICENSE +1 -0
  33. {vector_inspector-0.3.9.dist-info → vector_inspector-0.3.12.dist-info}/entry_points.txt +0 -0
@@ -1,15 +1,28 @@
1
1
  """Dialog for backup and restore operations."""
2
2
 
3
- from typing import Optional
3
+ from pathlib import Path
4
+
5
+ from PySide6.QtCore import Qt
4
6
  from PySide6.QtWidgets import (
5
- QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget,
6
- QPushButton, QLabel, QListWidget, QListWidgetItem, QFileDialog,
7
- QMessageBox, QCheckBox, QLineEdit, QGroupBox, QFormLayout, QApplication
7
+ QApplication,
8
+ QCheckBox,
9
+ QDialog,
10
+ QFileDialog,
11
+ QFormLayout,
12
+ QGroupBox,
13
+ QHBoxLayout,
14
+ QLabel,
15
+ QLineEdit,
16
+ QListWidget,
17
+ QListWidgetItem,
18
+ QMessageBox,
19
+ QPushButton,
20
+ QTabWidget,
21
+ QVBoxLayout,
22
+ QWidget,
8
23
  )
9
- from PySide6.QtCore import Qt
10
- from pathlib import Path
11
24
 
12
- from vector_inspector.core.connections.base_connection import VectorDBConnection
25
+ from vector_inspector.core.connection_manager import ConnectionInstance
13
26
  from vector_inspector.services.backup_restore_service import BackupRestoreService
14
27
  from vector_inspector.services.settings_service import SettingsService
15
28
  from vector_inspector.ui.components.loading_dialog import LoadingDialog
@@ -17,80 +30,84 @@ from vector_inspector.ui.components.loading_dialog import LoadingDialog
17
30
 
18
31
  class BackupRestoreDialog(QDialog):
19
32
  """Dialog for managing backups and restores."""
20
-
21
- def __init__(self, connection: VectorDBConnection, collection_name: str = "", parent=None):
33
+
34
+ def __init__(self, connection: ConnectionInstance, collection_name: str = "", parent=None):
22
35
  super().__init__(parent)
36
+ # Expects a ConnectionInstance wrapper; services access the underlying
37
+ # raw database connection via `.database` when needed.
23
38
  self.connection = connection
24
39
  self.collection_name = collection_name
25
40
  self.backup_service = BackupRestoreService()
26
41
  self.settings_service = SettingsService()
27
-
42
+
28
43
  # Load backup directory from settings or use default
29
44
  default_backup_dir = str(Path.home() / "vector-viewer-backups")
30
45
  self.backup_dir = self.settings_service.get("backup_directory", default_backup_dir)
31
-
46
+
32
47
  self.loading_dialog = LoadingDialog("Processing...", self)
33
-
48
+
34
49
  self.setWindowTitle("Backup & Restore")
35
50
  self.setMinimumSize(600, 500)
36
-
51
+
37
52
  self._setup_ui()
38
53
  self._refresh_backups_list()
39
-
54
+
40
55
  def _setup_ui(self):
41
56
  """Setup dialog UI."""
42
57
  layout = QVBoxLayout(self)
43
-
58
+
44
59
  # Tabs for backup and restore
45
60
  tabs = QTabWidget()
46
-
61
+
47
62
  # Backup tab
48
63
  backup_tab = self._create_backup_tab()
49
64
  tabs.addTab(backup_tab, "Create Backup")
50
-
65
+
51
66
  # Restore tab
52
67
  restore_tab = self._create_restore_tab()
53
68
  tabs.addTab(restore_tab, "Restore from Backup")
54
-
69
+
55
70
  layout.addWidget(tabs)
56
-
71
+
57
72
  # Close button
58
73
  close_button = QPushButton("Close")
59
74
  close_button.clicked.connect(self.accept)
60
75
  layout.addWidget(close_button, alignment=Qt.AlignRight)
61
-
76
+
62
77
  def _create_backup_tab(self) -> QWidget:
63
78
  """Create the backup tab."""
64
79
  widget = QWidget()
65
80
  layout = QVBoxLayout(widget)
66
-
81
+
67
82
  # Collection selection
68
83
  collection_group = QGroupBox("Backup Configuration")
69
84
  collection_layout = QFormLayout()
70
-
85
+
71
86
  # Collection name
72
- collection_layout.addRow("Collection:", QLabel(self.collection_name or "No collection selected"))
73
-
87
+ collection_layout.addRow(
88
+ "Collection:", QLabel(self.collection_name or "No collection selected")
89
+ )
90
+
74
91
  # Backup directory
75
92
  dir_layout = QHBoxLayout()
76
93
  self.backup_dir_input = QLineEdit(self.backup_dir)
77
94
  self.backup_dir_input.setReadOnly(True)
78
95
  dir_layout.addWidget(self.backup_dir_input)
79
-
96
+
80
97
  browse_btn = QPushButton("Browse...")
81
98
  browse_btn.clicked.connect(self._select_backup_dir)
82
99
  dir_layout.addWidget(browse_btn)
83
-
100
+
84
101
  collection_layout.addRow("Backup Directory:", dir_layout)
85
-
102
+
86
103
  # Options
87
104
  self.include_embeddings_check = QCheckBox("Include embedding vectors (larger file size)")
88
105
  self.include_embeddings_check.setChecked(True)
89
106
  collection_layout.addRow("Options:", self.include_embeddings_check)
90
-
107
+
91
108
  collection_group.setLayout(collection_layout)
92
109
  layout.addWidget(collection_group)
93
-
110
+
94
111
  # Info label
95
112
  info_label = QLabel(
96
113
  "Backup will create a compressed archive containing all collection data, "
@@ -99,38 +116,38 @@ class BackupRestoreDialog(QDialog):
99
116
  info_label.setWordWrap(True)
100
117
  info_label.setStyleSheet("color: gray; font-style: italic;")
101
118
  layout.addWidget(info_label)
102
-
119
+
103
120
  layout.addStretch()
104
-
121
+
105
122
  # Create backup button
106
123
  backup_button = QPushButton("Create Backup")
107
124
  backup_button.clicked.connect(self._create_backup)
108
125
  backup_button.setStyleSheet("QPushButton { font-weight: bold; padding: 8px; }")
109
126
  layout.addWidget(backup_button)
110
-
127
+
111
128
  return widget
112
-
129
+
113
130
  def _create_restore_tab(self) -> QWidget:
114
131
  """Create the restore tab."""
115
132
  widget = QWidget()
116
133
  layout = QVBoxLayout(widget)
117
-
134
+
118
135
  # Backup list
119
136
  layout.addWidget(QLabel("Available Backups:"))
120
-
137
+
121
138
  self.backups_list = QListWidget()
122
139
  self.backups_list.itemSelectionChanged.connect(self._on_backup_selected)
123
140
  layout.addWidget(self.backups_list)
124
-
141
+
125
142
  # Refresh button
126
143
  refresh_btn = QPushButton("Refresh List")
127
144
  refresh_btn.clicked.connect(self._refresh_backups_list)
128
145
  layout.addWidget(refresh_btn)
129
-
146
+
130
147
  # Restore options
131
148
  options_group = QGroupBox("Restore Options")
132
149
  options_layout = QVBoxLayout()
133
-
150
+
134
151
  # New collection name
135
152
  name_layout = QHBoxLayout()
136
153
  name_layout.addWidget(QLabel("Restore as:"))
@@ -138,150 +155,156 @@ class BackupRestoreDialog(QDialog):
138
155
  self.restore_name_input.setPlaceholderText("Leave empty to use original name")
139
156
  name_layout.addWidget(self.restore_name_input)
140
157
  options_layout.addLayout(name_layout)
141
-
158
+
142
159
  # Overwrite checkbox
143
160
  self.overwrite_check = QCheckBox("Overwrite if collection exists")
144
161
  self.overwrite_check.setChecked(False)
145
162
  options_layout.addWidget(self.overwrite_check)
146
-
163
+
147
164
  options_group.setLayout(options_layout)
148
165
  layout.addWidget(options_group)
149
-
166
+
150
167
  # Restore and delete buttons
151
168
  button_layout = QHBoxLayout()
152
-
169
+
153
170
  self.restore_button = QPushButton("Restore Selected")
154
171
  self.restore_button.clicked.connect(self._restore_backup)
155
172
  self.restore_button.setEnabled(False)
156
173
  self.restore_button.setStyleSheet("QPushButton { font-weight: bold; padding: 8px; }")
157
174
  button_layout.addWidget(self.restore_button)
158
-
175
+
159
176
  self.delete_backup_button = QPushButton("Delete Selected")
160
177
  self.delete_backup_button.clicked.connect(self._delete_backup)
161
178
  self.delete_backup_button.setEnabled(False)
162
179
  button_layout.addWidget(self.delete_backup_button)
163
-
180
+
164
181
  layout.addLayout(button_layout)
165
-
182
+
166
183
  return widget
167
-
184
+
168
185
  def _select_backup_dir(self):
169
186
  """Select backup directory."""
170
187
  dir_path = QFileDialog.getExistingDirectory(
171
- self,
172
- "Select Backup Directory",
173
- self.backup_dir
188
+ self, "Select Backup Directory", self.backup_dir
174
189
  )
175
-
190
+
176
191
  if dir_path:
177
192
  self.backup_dir = dir_path
178
193
  self.backup_dir_input.setText(dir_path)
179
-
194
+
180
195
  # Save to settings
181
196
  self.settings_service.set("backup_directory", dir_path)
182
-
197
+
183
198
  self._refresh_backups_list()
184
-
199
+
185
200
  def _create_backup(self):
186
201
  """Create a backup of the current collection."""
187
202
  if not self.collection_name:
188
203
  QMessageBox.warning(self, "No Collection", "No collection selected for backup.")
189
204
  return
190
-
205
+
191
206
  # Create backup
192
207
  include_embeddings = self.include_embeddings_check.isChecked()
193
-
208
+
194
209
  self.loading_dialog.show_loading("Creating backup...")
195
210
  QApplication.processEvents()
196
-
211
+
197
212
  try:
198
213
  backup_path = self.backup_service.backup_collection(
199
- self.connection,
214
+ self.connection.database,
200
215
  self.collection_name,
201
216
  self.backup_dir,
202
- include_embeddings=include_embeddings
217
+ include_embeddings=include_embeddings,
218
+ profile_name=self.connection.name,
203
219
  )
204
220
  finally:
205
221
  self.loading_dialog.hide_loading()
206
-
222
+
207
223
  if backup_path:
208
224
  QMessageBox.information(
209
- self,
210
- "Backup Successful",
211
- f"Backup created successfully:\n{backup_path}"
225
+ self, "Backup Successful", f"Backup created successfully:\n{backup_path}"
212
226
  )
213
227
  self._refresh_backups_list()
214
228
  else:
215
229
  QMessageBox.warning(self, "Backup Failed", "Failed to create backup.")
216
-
230
+
217
231
  def _refresh_backups_list(self):
218
232
  """Refresh the list of available backups."""
219
233
  self.backups_list.clear()
220
-
234
+
221
235
  backups = self.backup_service.list_backups(self.backup_dir)
222
-
236
+
223
237
  for backup in backups:
224
238
  # Format file size
225
239
  size_mb = backup["file_size"] / (1024 * 1024)
226
-
240
+
227
241
  item_text = (
228
242
  f"{backup['collection_name']} - {backup['timestamp']}\n"
229
243
  f" Items: {backup['item_count']}, Size: {size_mb:.2f} MB\n"
230
244
  f" File: {backup['file_name']}"
231
245
  )
232
-
246
+
233
247
  item = QListWidgetItem(item_text)
234
248
  item.setData(Qt.UserRole, backup["file_path"])
235
249
  self.backups_list.addItem(item)
236
-
250
+
237
251
  if not backups:
238
252
  item = QListWidgetItem("No backups found in directory")
239
253
  item.setFlags(Qt.NoItemFlags)
240
254
  self.backups_list.addItem(item)
241
-
255
+
242
256
  def _on_backup_selected(self):
243
257
  """Handle backup selection."""
244
258
  has_selection = len(self.backups_list.selectedItems()) > 0
245
259
  self.restore_button.setEnabled(has_selection)
246
260
  self.delete_backup_button.setEnabled(has_selection)
247
-
261
+
248
262
  def _restore_backup(self):
249
263
  """Restore a backup."""
250
264
  selected_items = self.backups_list.selectedItems()
251
265
  if not selected_items:
252
266
  return
253
-
267
+
254
268
  backup_file = selected_items[0].data(Qt.UserRole)
255
269
  if not backup_file:
256
270
  return
257
-
271
+
258
272
  # Read backup metadata to get original collection name
259
273
  try:
260
- import zipfile
261
274
  import json
262
- with zipfile.ZipFile(backup_file, 'r') as zipf:
263
- metadata_str = zipf.read('metadata.json').decode('utf-8')
275
+ import zipfile
276
+
277
+ with zipfile.ZipFile(backup_file, "r") as zipf:
278
+ metadata_str = zipf.read("metadata.json").decode("utf-8")
264
279
  metadata = json.loads(metadata_str)
265
280
  original_name = metadata.get("collection_name", "unknown")
266
281
  except Exception as e:
267
282
  QMessageBox.warning(self, "Error", f"Failed to read backup metadata: {e}")
268
283
  return
269
-
284
+
270
285
  # Get restore options
271
286
  restore_name = self.restore_name_input.text().strip()
272
287
  overwrite = self.overwrite_check.isChecked()
273
-
288
+
274
289
  # Determine final collection name
275
290
  final_name = restore_name if restore_name else original_name
276
-
291
+
277
292
  # Build confirmation message
278
293
  if restore_name:
279
294
  msg = f"Restore backup to NEW collection: '{final_name}'"
280
295
  else:
281
296
  msg = f"Restore backup to ORIGINAL collection: '{final_name}'"
282
-
297
+
283
298
  # Check if collection exists
284
- existing_collections = self.connection.list_collections()
299
+ # Extract the underlying database connection from the ConnectionInstance wrapper.
300
+ actual_conn = getattr(self.connection, "database", self.connection)
301
+ if hasattr(actual_conn, "list_collections"):
302
+ try:
303
+ existing_collections = actual_conn.list_collections()
304
+ except Exception:
305
+ existing_collections = getattr(self.connection, "collections", [])
306
+ else:
307
+ existing_collections = getattr(self.connection, "collections", [])
285
308
  if final_name in existing_collections:
286
309
  if overwrite:
287
310
  msg += f"\n\n⚠️ WARNING: This will DELETE and replace the existing collection '{final_name}'!"
@@ -290,58 +313,149 @@ class BackupRestoreDialog(QDialog):
290
313
  self,
291
314
  "Collection Exists",
292
315
  f"Collection '{final_name}' already exists.\n\n"
293
- f"Check 'Overwrite existing collection' to replace it, or enter a different name."
316
+ f"Check 'Overwrite existing collection' to replace it, or enter a different name.",
294
317
  )
295
318
  return
296
-
319
+
297
320
  # Confirm
298
- reply = QMessageBox.question(
299
- self,
300
- "Confirm Restore",
301
- msg,
302
- QMessageBox.Yes | QMessageBox.No
303
- )
304
-
321
+ reply = QMessageBox.question(self, "Confirm Restore", msg, QMessageBox.Yes | QMessageBox.No)
322
+
305
323
  if reply != QMessageBox.Yes:
306
324
  return
307
-
325
+
326
+ # If the backup included embeddings, ask user how to handle them
327
+ recompute_choice: bool | None = None
328
+ try:
329
+ import json
330
+ import zipfile
331
+
332
+ with zipfile.ZipFile(backup_file, "r") as zipf:
333
+ metadata_str = zipf.read("metadata.json").decode("utf-8")
334
+ metadata = json.loads(metadata_str)
335
+ include_embeddings = metadata.get("include_embeddings", False)
336
+ has_model = bool(metadata.get("embedding_model"))
337
+ embedding_model = metadata.get("embedding_model", "unknown")
338
+
339
+ if include_embeddings:
340
+ # Present three options: Use stored (default), Recompute, or Omit
341
+ from PySide6.QtWidgets import (
342
+ QDialog,
343
+ QHBoxLayout,
344
+ QLabel,
345
+ QPushButton,
346
+ QRadioButton,
347
+ QVBoxLayout,
348
+ )
349
+
350
+ dialog = QDialog(self)
351
+ dialog.setWindowTitle("Restore Embeddings")
352
+ dialog.setMinimumWidth(500)
353
+ layout = QVBoxLayout(dialog)
354
+
355
+ # Explanation
356
+ if has_model:
357
+ info_text = (
358
+ f"This backup includes {metadata.get('item_count', 0)} embedding vectors.\n"
359
+ f"Recorded model: {embedding_model}\n\n"
360
+ "How would you like to handle the embeddings?"
361
+ )
362
+ else:
363
+ info_text = (
364
+ f"This backup includes {metadata.get('item_count', 0)} embedding vectors,\n"
365
+ "but no embedding model was recorded.\n\n"
366
+ "How would you like to handle the embeddings?"
367
+ )
368
+
369
+ info_label = QLabel(info_text)
370
+ info_label.setWordWrap(True)
371
+ layout.addWidget(info_label)
372
+
373
+ # Radio buttons for options
374
+ use_stored_radio = QRadioButton(
375
+ "Use stored embeddings (recommended for new collections)"
376
+ )
377
+ use_stored_radio.setChecked(True) # Default option
378
+ layout.addWidget(use_stored_radio)
379
+
380
+ recompute_radio = QRadioButton("Recompute embeddings from documents")
381
+ if has_model:
382
+ recompute_radio.setToolTip(f"Will use model: {embedding_model}")
383
+ else:
384
+ recompute_radio.setToolTip(
385
+ "Will attempt using your current embedding configuration"
386
+ )
387
+ layout.addWidget(recompute_radio)
388
+
389
+ omit_radio = QRadioButton("Omit embeddings (documents and metadata only)")
390
+ layout.addWidget(omit_radio)
391
+
392
+ # Buttons
393
+ button_layout = QHBoxLayout()
394
+ ok_button = QPushButton("OK")
395
+ cancel_button = QPushButton("Cancel")
396
+ ok_button.clicked.connect(dialog.accept)
397
+ cancel_button.clicked.connect(dialog.reject)
398
+ button_layout.addStretch()
399
+ button_layout.addWidget(ok_button)
400
+ button_layout.addWidget(cancel_button)
401
+ layout.addLayout(button_layout)
402
+
403
+ # Show dialog and get choice
404
+ if dialog.exec() != QDialog.Accepted:
405
+ return
406
+
407
+ if use_stored_radio.isChecked():
408
+ recompute_choice = None # Use stored embeddings
409
+ elif recompute_radio.isChecked():
410
+ recompute_choice = True # Recompute
411
+ else: # omit_radio.isChecked()
412
+ recompute_choice = False # Omit
413
+ except Exception:
414
+ recompute_choice = None # Default to using stored embeddings
415
+
308
416
  self.loading_dialog.show_loading("Restoring backup...")
309
417
  QApplication.processEvents()
310
-
418
+
311
419
  try:
312
- # Restore
420
+ # Restore (pass low-level connection to service)
313
421
  success = self.backup_service.restore_collection(
314
- self.connection,
422
+ self.connection.database,
315
423
  backup_file,
316
424
  collection_name=restore_name if restore_name else None,
317
- overwrite=overwrite
425
+ overwrite=overwrite,
426
+ recompute_embeddings=recompute_choice,
427
+ profile_name=self.connection.name,
318
428
  )
319
429
  finally:
320
430
  self.loading_dialog.hide_loading()
321
-
431
+
322
432
  if success:
323
- QMessageBox.information(self, "Restore Successful", f"Backup restored successfully to collection '{final_name}'.")
433
+ QMessageBox.information(
434
+ self,
435
+ "Restore Successful",
436
+ f"Backup restored successfully to collection '{final_name}'.",
437
+ )
324
438
  else:
325
439
  QMessageBox.warning(self, "Restore Failed", "Failed to restore backup.")
326
-
440
+
327
441
  def _delete_backup(self):
328
442
  """Delete a backup file."""
329
443
  selected_items = self.backups_list.selectedItems()
330
444
  if not selected_items:
331
445
  return
332
-
446
+
333
447
  backup_file = selected_items[0].data(Qt.UserRole)
334
448
  if not backup_file:
335
449
  return
336
-
450
+
337
451
  # Confirm deletion
338
452
  reply = QMessageBox.question(
339
453
  self,
340
454
  "Confirm Deletion",
341
455
  f"Delete this backup file?\n{Path(backup_file).name}",
342
- QMessageBox.Yes | QMessageBox.No
456
+ QMessageBox.Yes | QMessageBox.No,
343
457
  )
344
-
458
+
345
459
  if reply == QMessageBox.Yes:
346
460
  if self.backup_service.delete_backup(backup_file):
347
461
  QMessageBox.information(self, "Deleted", "Backup deleted successfully.")