Initial Codes
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# models package
|
||||
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
Connection profile dataclass and registry.
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
import uuid
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConnectionProfile:
|
||||
name: str
|
||||
db_type: str # mysql | postgresql | sqlite | mssql
|
||||
host: str = "localhost"
|
||||
port: int = 3306
|
||||
database: str = ""
|
||||
username: str = ""
|
||||
color: str = "#89b4fa"
|
||||
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||||
ssl: bool = False
|
||||
ssl_cert: str = ""
|
||||
ssl_key: str = ""
|
||||
ssl_ca: str = ""
|
||||
connection_timeout: int = 30
|
||||
|
||||
DB_PORTS = {
|
||||
"mysql": 3306,
|
||||
"postgresql": 5432,
|
||||
"sqlite": 0,
|
||||
"mssql": 1433,
|
||||
}
|
||||
|
||||
DB_DISPLAY = {
|
||||
"mysql": "MySQL",
|
||||
"postgresql": "PostgreSQL",
|
||||
"sqlite": "SQLite",
|
||||
"mssql": "SQL Server",
|
||||
}
|
||||
|
||||
@property
|
||||
def db_type_display(self) -> str:
|
||||
return self.DB_DISPLAY.get(self.db_type, self.db_type)
|
||||
|
||||
@property
|
||||
def default_port(self) -> int:
|
||||
return self.DB_PORTS.get(self.db_type, 0)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"db_type": self.db_type,
|
||||
"host": self.host,
|
||||
"port": self.port,
|
||||
"database": self.database,
|
||||
"username": self.username,
|
||||
"color": self.color,
|
||||
"ssl": self.ssl,
|
||||
"ssl_cert": self.ssl_cert,
|
||||
"ssl_key": self.ssl_key,
|
||||
"ssl_ca": self.ssl_ca,
|
||||
"connection_timeout": self.connection_timeout,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "ConnectionProfile":
|
||||
return cls(
|
||||
id=data.get("id", str(uuid.uuid4())),
|
||||
name=data.get("name", "Untitled"),
|
||||
db_type=data.get("db_type", "mysql"),
|
||||
host=data.get("host", "localhost"),
|
||||
port=data.get("port", 3306),
|
||||
database=data.get("database", ""),
|
||||
username=data.get("username", ""),
|
||||
color=data.get("color", "#89b4fa"),
|
||||
ssl=data.get("ssl", False),
|
||||
ssl_cert=data.get("ssl_cert", ""),
|
||||
ssl_key=data.get("ssl_key", ""),
|
||||
ssl_ca=data.get("ssl_ca", ""),
|
||||
connection_timeout=data.get("connection_timeout", 30),
|
||||
)
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
QAbstractTableModel that wraps a list of plain tuples (rows) for display
|
||||
in a QTableView. Supports sorting and in-place data refresh.
|
||||
"""
|
||||
from PyQt6.QtCore import (
|
||||
QAbstractTableModel, QModelIndex, Qt, QSortFilterProxyModel
|
||||
)
|
||||
from PyQt6.QtGui import QColor, QFont
|
||||
|
||||
|
||||
class ResultTableModel(QAbstractTableModel):
|
||||
"""Immutable result-set model — replaces data via set_data()."""
|
||||
|
||||
NULL_COLOR = QColor("#6c7086") # muted grey for NULL
|
||||
NUM_ALIGN = Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||
TEXT_ALIGN = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
|
||||
|
||||
_NUMERIC_TYPES = (int, float)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._columns: list = []
|
||||
self._rows: list = []
|
||||
|
||||
# ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
def set_data(self, columns: list, rows: list) -> None:
|
||||
self.beginResetModel()
|
||||
self._columns = list(columns)
|
||||
self._rows = [tuple(r) for r in rows]
|
||||
self.endResetModel()
|
||||
|
||||
def clear(self) -> None:
|
||||
self.set_data([], [])
|
||||
|
||||
def get_row(self, row: int) -> tuple:
|
||||
return self._rows[row]
|
||||
|
||||
def column_names(self) -> list:
|
||||
return list(self._columns)
|
||||
|
||||
def export_csv(self, filepath: str, delimiter: str = ",") -> None:
|
||||
import csv
|
||||
with open(filepath, "w", newline="", encoding="utf-8") as f:
|
||||
writer = csv.writer(f, delimiter=delimiter)
|
||||
writer.writerow(self._columns)
|
||||
writer.writerows(self._rows)
|
||||
|
||||
def export_json(self, filepath: str) -> None:
|
||||
import json
|
||||
records = [dict(zip(self._columns, row)) for row in self._rows]
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
json.dump(records, f, indent=2, default=str)
|
||||
|
||||
def export_sql(self, filepath: str, table_name: str = "table") -> None:
|
||||
with open(filepath, "w", encoding="utf-8") as f:
|
||||
cols = ", ".join(self._columns)
|
||||
for row in self._rows:
|
||||
vals = ", ".join(
|
||||
"NULL" if v is None else f"'{str(v).replace(chr(39), chr(39)*2)}'"
|
||||
for v in row
|
||||
)
|
||||
f.write(f"INSERT INTO {table_name} ({cols}) VALUES ({vals});\n")
|
||||
|
||||
# ── QAbstractTableModel interface ─────────────────────────────────────────
|
||||
|
||||
def rowCount(self, parent=QModelIndex()) -> int:
|
||||
return len(self._rows)
|
||||
|
||||
def columnCount(self, parent=QModelIndex()) -> int:
|
||||
return len(self._columns)
|
||||
|
||||
def headerData(self, section: int, orientation, role=Qt.ItemDataRole.DisplayRole):
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
if orientation == Qt.Orientation.Horizontal:
|
||||
return self._columns[section] if section < len(self._columns) else ""
|
||||
else:
|
||||
return str(section + 1)
|
||||
if role == Qt.ItemDataRole.FontRole and orientation == Qt.Orientation.Horizontal:
|
||||
f = QFont()
|
||||
f.setBold(True)
|
||||
return f
|
||||
return None
|
||||
|
||||
def data(self, index: QModelIndex, role=Qt.ItemDataRole.DisplayRole):
|
||||
if not index.isValid():
|
||||
return None
|
||||
row, col = index.row(), index.column()
|
||||
if row >= len(self._rows) or col >= len(self._columns):
|
||||
return None
|
||||
value = self._rows[row][col]
|
||||
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
if value is None:
|
||||
return "NULL"
|
||||
return str(value)
|
||||
|
||||
if role == Qt.ItemDataRole.ForegroundRole and value is None:
|
||||
return self.NULL_COLOR
|
||||
|
||||
if role == Qt.ItemDataRole.TextAlignmentRole:
|
||||
if isinstance(value, self._NUMERIC_TYPES):
|
||||
return int(self.NUM_ALIGN)
|
||||
return int(self.TEXT_ALIGN)
|
||||
|
||||
if role == Qt.ItemDataRole.UserRole:
|
||||
return value # raw Python value
|
||||
|
||||
return None
|
||||
|
||||
def flags(self, index: QModelIndex):
|
||||
return Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable
|
||||
Reference in New Issue
Block a user