sqlshell 0.1.8__py3-none-any.whl → 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.

Potentially problematic release.


This version of sqlshell might be problematic. Click here for more details.

@@ -0,0 +1,6 @@
1
+ """UI components for SQLShell application."""
2
+
3
+ from sqlshell.ui.filter_header import FilterHeader
4
+ from sqlshell.ui.bar_chart_delegate import BarChartDelegate
5
+
6
+ __all__ = ['FilterHeader', 'BarChartDelegate']
@@ -0,0 +1,49 @@
1
+ from PyQt6.QtWidgets import QStyledItemDelegate
2
+ from PyQt6.QtCore import Qt, QRect
3
+ from PyQt6.QtGui import QColor
4
+
5
+ class BarChartDelegate(QStyledItemDelegate):
6
+ def __init__(self, parent=None):
7
+ super().__init__(parent)
8
+ self.min_val = 0
9
+ self.max_val = 1
10
+ self.bar_color = QColor("#3498DB")
11
+
12
+ def set_range(self, min_val, max_val):
13
+ self.min_val = min_val
14
+ self.max_val = max_val
15
+
16
+ def paint(self, painter, option, index):
17
+ # Draw the default background
18
+ super().paint(painter, option, index)
19
+
20
+ try:
21
+ text = index.data()
22
+ value = float(text.replace(',', ''))
23
+
24
+ # Calculate normalized value
25
+ range_val = self.max_val - self.min_val if self.max_val != self.min_val else 1
26
+ normalized = (value - self.min_val) / range_val
27
+
28
+ # Define bar dimensions
29
+ bar_height = 16
30
+ max_bar_width = 100
31
+ bar_width = max(5, int(max_bar_width * normalized))
32
+
33
+ # Calculate positions
34
+ text_width = option.fontMetrics.horizontalAdvance(text) + 10
35
+ bar_x = option.rect.left() + text_width + 10
36
+ bar_y = option.rect.center().y() - bar_height // 2
37
+
38
+ # Draw the bar
39
+ bar_rect = QRect(bar_x, bar_y, bar_width, bar_height)
40
+ painter.fillRect(bar_rect, self.bar_color)
41
+
42
+ # Draw the text
43
+ text_rect = QRect(option.rect.left() + 4, option.rect.top(),
44
+ text_width, option.rect.height())
45
+ painter.drawText(text_rect, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, text)
46
+
47
+ except (ValueError, AttributeError):
48
+ # If not a number, just draw the text
49
+ super().paint(painter, option, index)
@@ -0,0 +1,403 @@
1
+ from PyQt6.QtWidgets import (QHeaderView, QMenu, QCheckBox, QWidgetAction,
2
+ QWidget, QVBoxLayout, QLineEdit, QHBoxLayout, QPushButton, QTableWidget)
3
+ from PyQt6.QtCore import Qt, QRect, QPoint
4
+ from PyQt6.QtGui import QColor, QFont, QPolygon, QPainterPath, QBrush
5
+
6
+ class FilterHeader(QHeaderView):
7
+ def __init__(self, parent=None):
8
+ super().__init__(Qt.Orientation.Horizontal, parent)
9
+ self.filter_buttons = []
10
+ self.active_filters = {} # Track active filters for each column
11
+ self.columns_with_bars = set() # Track which columns show bar charts
12
+ self.bar_delegates = {} # Store delegates for columns with bars
13
+ self.setSectionsClickable(True)
14
+ self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
15
+ self.customContextMenuRequested.connect(self.show_header_context_menu)
16
+ self.main_window = None # Store reference to main window
17
+ self.filter_icon_color = QColor("#3498DB") # Bright blue color for filter icon
18
+
19
+ def toggle_bar_chart(self, column_index):
20
+ """Toggle bar chart visualization for a column"""
21
+ table = self.parent()
22
+ if not table:
23
+ return
24
+
25
+ if column_index in self.columns_with_bars:
26
+ # Remove bars
27
+ self.columns_with_bars.remove(column_index)
28
+ if column_index in self.bar_delegates:
29
+ table.setItemDelegateForColumn(column_index, None)
30
+ del self.bar_delegates[column_index]
31
+ else:
32
+ # Add bars
33
+ self.columns_with_bars.add(column_index)
34
+
35
+ # Get all values for normalization
36
+ values = []
37
+ for row in range(table.rowCount()):
38
+ item = table.item(row, column_index)
39
+ if item:
40
+ try:
41
+ value = float(item.text().replace(',', ''))
42
+ values.append(value)
43
+ except ValueError:
44
+ continue
45
+
46
+ if not values:
47
+ return
48
+
49
+ # Calculate min and max for normalization
50
+ min_val = min(values)
51
+ max_val = max(values)
52
+
53
+ # Import BarChartDelegate here to avoid circular imports
54
+ from sqlshell.ui.bar_chart_delegate import BarChartDelegate
55
+
56
+ # Create and set up delegate
57
+ delegate = BarChartDelegate(table)
58
+ delegate.set_range(min_val, max_val)
59
+ self.bar_delegates[column_index] = delegate
60
+ table.setItemDelegateForColumn(column_index, delegate)
61
+
62
+ # Update the view
63
+ table.viewport().update()
64
+
65
+ def show_header_context_menu(self, pos):
66
+ """Show context menu for header section"""
67
+ logical_index = self.logicalIndexAt(pos)
68
+ if logical_index < 0:
69
+ return
70
+
71
+ # Create context menu
72
+ context_menu = QMenu(self)
73
+ context_menu.setStyleSheet("""
74
+ QMenu {
75
+ background-color: white;
76
+ border: 1px solid #BDC3C7;
77
+ padding: 5px;
78
+ }
79
+ QMenu::item {
80
+ padding: 5px 20px;
81
+ }
82
+ QMenu::item:selected {
83
+ background-color: #3498DB;
84
+ color: white;
85
+ }
86
+ """)
87
+
88
+ # Add sort actions
89
+ sort_asc_action = context_menu.addAction("Sort Ascending")
90
+ sort_desc_action = context_menu.addAction("Sort Descending")
91
+ context_menu.addSeparator()
92
+ filter_action = context_menu.addAction("Filter...")
93
+
94
+ # Add bar chart action if column is numeric
95
+ table = self.parent()
96
+ if table and table.rowCount() > 0:
97
+ try:
98
+ # Check if column contains numeric values
99
+ sample_value = table.item(0, logical_index).text()
100
+ float(sample_value.replace(',', '')) # Try converting to float
101
+
102
+ context_menu.addSeparator()
103
+ toggle_bar_action = context_menu.addAction(
104
+ "Remove Bar Chart" if logical_index in self.columns_with_bars
105
+ else "Add Bar Chart"
106
+ )
107
+ except (ValueError, AttributeError):
108
+ toggle_bar_action = None
109
+ else:
110
+ toggle_bar_action = None
111
+
112
+ # Show menu and get selected action
113
+ action = context_menu.exec(self.mapToGlobal(pos))
114
+
115
+ if not action:
116
+ return
117
+
118
+ table = self.parent()
119
+ if not table:
120
+ return
121
+
122
+ if action == sort_asc_action:
123
+ table.sortItems(logical_index, Qt.SortOrder.AscendingOrder)
124
+ elif action == sort_desc_action:
125
+ table.sortItems(logical_index, Qt.SortOrder.DescendingOrder)
126
+ elif action == filter_action:
127
+ self.show_filter_menu(logical_index)
128
+ elif action == toggle_bar_action:
129
+ self.toggle_bar_chart(logical_index)
130
+
131
+ def set_main_window(self, window):
132
+ """Set the reference to the main window"""
133
+ self.main_window = window
134
+
135
+ def paintSection(self, painter, rect, logical_index):
136
+ """Override paint section to add filter indicator"""
137
+ super().paintSection(painter, rect, logical_index)
138
+
139
+ if logical_index in self.active_filters:
140
+ # Draw background highlight for filtered columns
141
+ highlight_color = QColor(52, 152, 219, 30) # Light blue background
142
+ painter.fillRect(rect, highlight_color)
143
+
144
+ # Make icon larger and more visible
145
+ icon_size = min(rect.height() - 8, 24) # Larger icon, but not too large
146
+ margin = 6
147
+ icon_rect = QRect(
148
+ rect.right() - icon_size - margin,
149
+ rect.top() + (rect.height() - icon_size) // 2,
150
+ icon_size,
151
+ icon_size
152
+ )
153
+
154
+ # Draw filter icon with improved visibility
155
+ painter.save()
156
+
157
+ # Set up the pen for better visibility
158
+ pen = painter.pen()
159
+ pen.setWidth(3) # Thicker lines
160
+ pen.setColor(self.filter_icon_color)
161
+ painter.setPen(pen)
162
+
163
+ # Calculate points for larger funnel shape
164
+ points = [
165
+ QPoint(icon_rect.left(), icon_rect.top()),
166
+ QPoint(icon_rect.right(), icon_rect.top()),
167
+ QPoint(icon_rect.center().x() + icon_size//3, icon_rect.center().y()),
168
+ QPoint(icon_rect.center().x() + icon_size//3, icon_rect.bottom()),
169
+ QPoint(icon_rect.center().x() - icon_size//3, icon_rect.bottom()),
170
+ QPoint(icon_rect.center().x() - icon_size//3, icon_rect.center().y()),
171
+ QPoint(icon_rect.left(), icon_rect.top())
172
+ ]
173
+
174
+ # Create and fill path
175
+ path = QPainterPath()
176
+ path.moveTo(float(points[0].x()), float(points[0].y()))
177
+ for point in points[1:]:
178
+ path.lineTo(float(point.x()), float(point.y()))
179
+
180
+ # Fill with semi-transparent blue
181
+ painter.fillPath(path, QBrush(QColor(52, 152, 219, 120))) # More opaque fill
182
+
183
+ # Draw outline
184
+ painter.drawPolyline(QPolygon(points))
185
+
186
+ # If multiple values are filtered, add a number
187
+ if len(self.active_filters[logical_index]) > 1:
188
+ # Draw number with better visibility
189
+ number_rect = QRect(icon_rect.left(), icon_rect.top(),
190
+ icon_rect.width(), icon_rect.height())
191
+ painter.setFont(QFont("Arial", icon_size//2, QFont.Weight.Bold))
192
+
193
+ # Draw text shadow for better contrast
194
+ painter.setPen(QColor("white"))
195
+ painter.drawText(number_rect.adjusted(1, 1, 1, 1),
196
+ Qt.AlignmentFlag.AlignCenter,
197
+ str(len(self.active_filters[logical_index])))
198
+
199
+ # Draw main text
200
+ painter.setPen(self.filter_icon_color)
201
+ painter.drawText(number_rect, Qt.AlignmentFlag.AlignCenter,
202
+ str(len(self.active_filters[logical_index])))
203
+
204
+ painter.restore()
205
+
206
+ # Draw a more visible indicator at the bottom of the header section
207
+ painter.save()
208
+ indicator_height = 3 # Thicker indicator line
209
+ indicator_rect = QRect(rect.left(), rect.bottom() - indicator_height,
210
+ rect.width(), indicator_height)
211
+ painter.fillRect(indicator_rect, self.filter_icon_color)
212
+ painter.restore()
213
+
214
+ def show_filter_menu(self, logical_index):
215
+ if not self.parent() or not isinstance(self.parent(), QTableWidget):
216
+ return
217
+
218
+ table = self.parent()
219
+ unique_values = set()
220
+
221
+ # Collect unique values from the column
222
+ for row in range(table.rowCount()):
223
+ item = table.item(row, logical_index)
224
+ if item and not table.isRowHidden(row):
225
+ unique_values.add(item.text())
226
+
227
+ # Create and show the filter menu
228
+ menu = QMenu(self)
229
+ menu.setStyleSheet("""
230
+ QMenu {
231
+ background-color: white;
232
+ border: 1px solid #BDC3C7;
233
+ padding: 5px;
234
+ }
235
+ QMenu::item {
236
+ padding: 5px 20px;
237
+ }
238
+ QMenu::item:selected {
239
+ background-color: #3498DB;
240
+ color: white;
241
+ }
242
+ QCheckBox {
243
+ padding: 5px;
244
+ }
245
+ QScrollArea {
246
+ border: none;
247
+ }
248
+ """)
249
+
250
+ # Add search box at the top
251
+ search_widget = QWidget(menu)
252
+ search_layout = QVBoxLayout(search_widget)
253
+ search_edit = QLineEdit(search_widget)
254
+ search_edit.setPlaceholderText("Search values...")
255
+ search_layout.addWidget(search_edit)
256
+
257
+ # Add action for search widget
258
+ search_action = QWidgetAction(menu)
259
+ search_action.setDefaultWidget(search_widget)
260
+ menu.addAction(search_action)
261
+ menu.addSeparator()
262
+
263
+ # Add "Select All" checkbox
264
+ select_all = QCheckBox("Select All", menu)
265
+ select_all.setChecked(True)
266
+ select_all_action = QWidgetAction(menu)
267
+ select_all_action.setDefaultWidget(select_all)
268
+ menu.addAction(select_all_action)
269
+ menu.addSeparator()
270
+
271
+ # Create scrollable area for checkboxes
272
+ scroll_widget = QWidget(menu)
273
+ scroll_layout = QVBoxLayout(scroll_widget)
274
+ scroll_layout.setSpacing(2)
275
+ scroll_layout.setContentsMargins(0, 0, 0, 0)
276
+
277
+ # Add checkboxes for unique values
278
+ value_checkboxes = {}
279
+ for value in sorted(unique_values):
280
+ checkbox = QCheckBox(str(value), scroll_widget)
281
+ # Set checked state based on active filters
282
+ checkbox.setChecked(logical_index not in self.active_filters or
283
+ value in self.active_filters[logical_index])
284
+ value_checkboxes[value] = checkbox
285
+ scroll_layout.addWidget(checkbox)
286
+
287
+ # Add scrollable area to menu
288
+ scroll_action = QWidgetAction(menu)
289
+ scroll_action.setDefaultWidget(scroll_widget)
290
+ menu.addAction(scroll_action)
291
+
292
+ # Connect search box to filter checkboxes
293
+ def filter_checkboxes(text):
294
+ for value, checkbox in value_checkboxes.items():
295
+ checkbox.setVisible(text.lower() in str(value).lower())
296
+
297
+ search_edit.textChanged.connect(filter_checkboxes)
298
+
299
+ # Connect select all to other checkboxes
300
+ def toggle_all(state):
301
+ for checkbox in value_checkboxes.values():
302
+ if not checkbox.isHidden(): # Only toggle visible checkboxes
303
+ checkbox.setChecked(state)
304
+
305
+ select_all.stateChanged.connect(toggle_all)
306
+
307
+ # Add Apply and Clear buttons
308
+ menu.addSeparator()
309
+ apply_button = QPushButton("Apply Filter", menu)
310
+ apply_button.setStyleSheet("""
311
+ QPushButton {
312
+ background-color: #2ECC71;
313
+ color: white;
314
+ border: none;
315
+ padding: 5px 15px;
316
+ border-radius: 3px;
317
+ }
318
+ QPushButton:hover {
319
+ background-color: #27AE60;
320
+ }
321
+ """)
322
+
323
+ clear_button = QPushButton("Clear Filter", menu)
324
+ clear_button.setStyleSheet("""
325
+ QPushButton {
326
+ background-color: #E74C3C;
327
+ color: white;
328
+ border: none;
329
+ padding: 5px 15px;
330
+ border-radius: 3px;
331
+ }
332
+ QPushButton:hover {
333
+ background-color: #C0392B;
334
+ }
335
+ """)
336
+
337
+ button_widget = QWidget(menu)
338
+ button_layout = QHBoxLayout(button_widget)
339
+ button_layout.addWidget(apply_button)
340
+ button_layout.addWidget(clear_button)
341
+
342
+ button_action = QWidgetAction(menu)
343
+ button_action.setDefaultWidget(button_widget)
344
+ menu.addAction(button_action)
345
+
346
+ def apply_filter():
347
+ # Get selected values
348
+ selected_values = {value for value, checkbox in value_checkboxes.items()
349
+ if checkbox.isChecked()}
350
+
351
+ if len(selected_values) < len(unique_values):
352
+ # Store active filter only if not all values are selected
353
+ self.active_filters[logical_index] = selected_values
354
+ else:
355
+ # Remove filter if all values are selected
356
+ self.active_filters.pop(logical_index, None)
357
+
358
+ # Apply all active filters
359
+ self.apply_all_filters(table)
360
+
361
+ menu.close()
362
+ self.updateSection(logical_index) # Redraw section to show/hide filter icon
363
+
364
+ def clear_filter():
365
+ # Remove filter for this column
366
+ if logical_index in self.active_filters:
367
+ del self.active_filters[logical_index]
368
+
369
+ # Apply remaining filters
370
+ self.apply_all_filters(table)
371
+
372
+ menu.close()
373
+ self.updateSection(logical_index) # Redraw section to hide filter icon
374
+
375
+ apply_button.clicked.connect(apply_filter)
376
+ clear_button.clicked.connect(clear_filter)
377
+
378
+ # Show menu under the header section
379
+ header_pos = self.mapToGlobal(self.geometry().bottomLeft())
380
+ header_pos.setX(header_pos.x() + self.sectionPosition(logical_index))
381
+ menu.exec(header_pos)
382
+
383
+ def apply_all_filters(self, table):
384
+ """Apply all active filters to the table"""
385
+ # Show all rows first
386
+ for row in range(table.rowCount()):
387
+ table.setRowHidden(row, False)
388
+
389
+ # Apply each active filter
390
+ for col_idx, allowed_values in self.active_filters.items():
391
+ for row in range(table.rowCount()):
392
+ item = table.item(row, col_idx)
393
+ if item and not table.isRowHidden(row):
394
+ table.setRowHidden(row, item.text() not in allowed_values)
395
+
396
+ # Update status bar with visible row count
397
+ if self.main_window:
398
+ visible_rows = sum(1 for row in range(table.rowCount())
399
+ if not table.isRowHidden(row))
400
+ total_filters = len(self.active_filters)
401
+ filter_text = f" ({total_filters} filter{'s' if total_filters != 1 else ''} active)" if total_filters > 0 else ""
402
+ self.main_window.statusBar().showMessage(
403
+ f"Showing {visible_rows:,} rows{filter_text}")
@@ -0,0 +1,8 @@
1
+ """
2
+ Utility functions for SQLShell
3
+ """
4
+
5
+ # Import profile_entropy for convenient access
6
+ from sqlshell.utils.profile_entropy import profile, visualize_profile, EntropyProfiler
7
+
8
+ # Empty init file to make the directory a package