""" Import CSV / JSON into a database table. Flow: 1. User picks a file (.csv or .json) 2. A preview of the first N rows is shown 3. User confirms → rows are inserted via the driver's insert_row() """ import csv import json import os from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLabel, QLineEdit, QPushButton, QComboBox, QTableWidget, QTableWidgetItem, QHeaderView, QDialogButtonBox, QFileDialog, QProgressBar, QMessageBox, QCheckBox, ) from PyQt6.QtCore import Qt, QThread, pyqtSignal _PREVIEW_ROWS = 50 # ── Background import worker ────────────────────────────────────────────────── class _ImportWorker(QThread): progress = pyqtSignal(int) # rows inserted so far finished = pyqtSignal(int) # total rows inserted error = pyqtSignal(str) def __init__(self, driver, database: str, table: str, rows: list, parent=None): super().__init__(parent) self._driver = driver self._database = database self._table = table self._rows = rows # list of dicts {col: value} def run(self): inserted = 0 try: for row in self._rows: self._driver.insert_row(self._database, self._table, row) inserted += 1 if inserted % 50 == 0: self.progress.emit(inserted) self.finished.emit(inserted) except Exception as e: self.error.emit(f"Row {inserted + 1}: {e}") # ── Dialog ──────────────────────────────────────────────────────────────────── class ImportDialog(QDialog): """Select a CSV or JSON file and import its contents into a table.""" def __init__(self, driver, database: str, table: str, parent=None): super().__init__(parent) self._driver = driver self._database = database self._table = table self._rows: list = [] # parsed rows ready for import self._worker = None self.setWindowTitle(f"Import into {table}") self.setModal(True) self.setMinimumSize(640, 480) self._build_ui() # ── UI ──────────────────────────────────────────────────────────────────── def _build_ui(self): root = QVBoxLayout(self) # File picker row file_row = QHBoxLayout() self._path_edit = QLineEdit() self._path_edit.setReadOnly(True) self._path_edit.setPlaceholderText("Select a .csv or .json file…") browse_btn = QPushButton("Browse…") browse_btn.setFixedWidth(80) browse_btn.clicked.connect(self._browse) file_row.addWidget(self._path_edit) file_row.addWidget(browse_btn) root.addLayout(file_row) # CSV options (hidden until a CSV is selected) self._csv_opts = QHBoxLayout() self._csv_opts_widget = self._build_csv_opts() root.addWidget(self._csv_opts_widget) self._csv_opts_widget.setVisible(False) # Skip-header checkbox self._header_cb = QCheckBox("First row is a header (CSV only)") self._header_cb.setChecked(True) self._header_cb.toggled.connect(self._reload_preview) root.addWidget(self._header_cb) # Preview table root.addWidget(QLabel("Preview (first 50 rows):")) self._preview = QTableWidget(0, 0) self._preview.setAlternatingRowColors(True) self._preview.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) self._preview.horizontalHeader().setSectionResizeMode( QHeaderView.ResizeMode.ResizeToContents ) self._preview.verticalHeader().setDefaultSectionSize(22) self._preview.setSelectionMode(QTableWidget.SelectionMode.NoSelection) root.addWidget(self._preview, 1) # Status / progress self._status_lbl = QLabel("") root.addWidget(self._status_lbl) self._progress = QProgressBar() self._progress.setVisible(False) root.addWidget(self._progress) # Buttons self._btns = QDialogButtonBox( QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel ) self._ok_btn = self._btns.button(QDialogButtonBox.StandardButton.Ok) self._ok_btn.setText("Import") self._ok_btn.setEnabled(False) self._btns.accepted.connect(self._start_import) self._btns.rejected.connect(self.reject) root.addWidget(self._btns) def _build_csv_opts(self) -> QHBoxLayout: from PyQt6.QtWidgets import QWidget w = QWidget() lay = QHBoxLayout(w) lay.setContentsMargins(0, 0, 0, 0) lay.addWidget(QLabel("Delimiter:")) self._delim_combo = QComboBox() self._delim_combo.addItems([", (comma)", "; (semicolon)", "\\t (tab)", "| (pipe)"]) self._delim_combo.setFixedWidth(140) self._delim_combo.currentIndexChanged.connect(self._reload_preview) lay.addWidget(self._delim_combo) lay.addStretch() return w # ── File loading ────────────────────────────────────────────────────────── def _browse(self): path, _ = QFileDialog.getOpenFileName( self, "Open file", "", "CSV / JSON files (*.csv *.json);;All files (*)" ) if not path: return self._path_edit.setText(path) ext = os.path.splitext(path)[1].lower() self._csv_opts_widget.setVisible(ext == ".csv") self._header_cb.setVisible(ext == ".csv") self._reload_preview() def _delimiter(self) -> str: mapping = {0: ",", 1: ";", 2: "\t", 3: "|"} return mapping.get(self._delim_combo.currentIndex(), ",") def _reload_preview(self): path = self._path_edit.text() if not path: return ext = os.path.splitext(path)[1].lower() try: if ext == ".csv": self._rows = self._parse_csv(path) elif ext == ".json": self._rows = self._parse_json(path) else: self._status_lbl.setText("Unsupported file type.") return except Exception as e: self._status_lbl.setText(f"Parse error: {e}") self._rows = [] self._ok_btn.setEnabled(False) return self._populate_preview(self._rows[:_PREVIEW_ROWS]) self._status_lbl.setText( f"{len(self._rows)} row(s) ready to import into " f"'{self._database}'.'{self._table}'" ) self._ok_btn.setEnabled(bool(self._rows)) def _parse_csv(self, path: str) -> list: rows = [] with open(path, newline="", encoding="utf-8-sig") as f: reader = csv.reader(f, delimiter=self._delimiter()) all_rows = list(reader) if not all_rows: return [] if self._header_cb.isChecked(): headers = all_rows[0] data_rows = all_rows[1:] else: headers = [f"col{i+1}" for i in range(len(all_rows[0]))] data_rows = all_rows for row in data_rows: # Pad short rows, truncate long ones padded = (row + [""] * len(headers))[: len(headers)] rows.append(dict(zip(headers, padded))) return rows def _parse_json(self, path: str) -> list: with open(path, encoding="utf-8") as f: data = json.load(f) if isinstance(data, list): return [r for r in data if isinstance(r, dict)] if isinstance(data, dict): # Support {rows: [...]} or {data: [...]} wrappers for key in ("rows", "data", "records", "items"): if isinstance(data.get(key), list): return data[key] raise ValueError("JSON must be an array of objects or {rows: [...]}") # ── Preview ─────────────────────────────────────────────────────────────── def _populate_preview(self, rows: list): if not rows: self._preview.setRowCount(0) self._preview.setColumnCount(0) return headers = list(rows[0].keys()) self._preview.setColumnCount(len(headers)) self._preview.setHorizontalHeaderLabels(headers) self._preview.setRowCount(len(rows)) for r, row in enumerate(rows): for c, key in enumerate(headers): val = row.get(key, "") self._preview.setItem( r, c, QTableWidgetItem("" if val is None else str(val)) ) # ── Import ──────────────────────────────────────────────────────────────── def _start_import(self): if not self._rows: return self._ok_btn.setEnabled(False) self._progress.setMaximum(len(self._rows)) self._progress.setValue(0) self._progress.setVisible(True) self._worker = _ImportWorker( self._driver, self._database, self._table, self._rows, parent=self ) self._worker.progress.connect(self._progress.setValue) self._worker.finished.connect(self._on_done) self._worker.error.connect(self._on_error) self._worker.start() def _on_done(self, count: int): self._progress.setValue(count) QMessageBox.information( self, "Import Complete", f"Successfully imported {count} row(s) into '{self._table}'." ) self.accept() def _on_error(self, msg: str): self._progress.setVisible(False) self._ok_btn.setEnabled(True) QMessageBox.critical(self, "Import Error", f"Import failed:\n{msg}")