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

sqlshell/__init__.py CHANGED
@@ -2,5 +2,7 @@
2
2
  SQLShell - A powerful SQL shell with GUI interface for data analysis
3
3
  """
4
4
 
5
- __version__ = "0.1.6"
6
- __author__ = "SQLShell Team"
5
+ __version__ = "0.1.8"
6
+ __author__ = "SQLShell Team"
7
+
8
+ # SQLShell package initialization
@@ -0,0 +1,50 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ from datetime import datetime, timedelta
4
+
5
+ def create_sales_data(num_records=1000):
6
+ """Create sample sales data"""
7
+ # Generate random dates within the last year
8
+ end_date = datetime.now()
9
+ start_date = end_date - timedelta(days=365)
10
+ dates = pd.date_range(start=start_date, end=end_date, periods=num_records)
11
+
12
+ # Generate random data
13
+ data = {
14
+ 'orderid': range(1, num_records + 1),
15
+ 'orderdate': dates,
16
+ 'customerid': np.random.randint(1, 101, num_records),
17
+ 'productid': np.random.randint(1, 51, num_records),
18
+ 'quantity': np.random.randint(1, 11, num_records),
19
+ 'unitprice': np.random.uniform(10.0, 1000.0, num_records).round(2)
20
+ }
21
+
22
+ return pd.DataFrame(data)
23
+
24
+ def create_customer_data(num_customers=100):
25
+ """Create sample customer data"""
26
+ # Generate random customer data
27
+ data = {
28
+ 'customerid': range(1, num_customers + 1),
29
+ 'customername': [f"Customer {i}" for i in range(1, num_customers + 1)],
30
+ 'email': [f"customer{i}@example.com" for i in range(1, num_customers + 1)],
31
+ 'country': np.random.choice(['USA', 'UK', 'Canada', 'Australia', 'Germany'], num_customers),
32
+ 'joindate': pd.date_range(start='2020-01-01', periods=num_customers).tolist()
33
+ }
34
+
35
+ return pd.DataFrame(data)
36
+
37
+ def create_product_data(num_products=50):
38
+ """Create sample product data"""
39
+ categories = ['Electronics', 'Books', 'Clothing', 'Home & Garden', 'Sports']
40
+
41
+ # Generate random product data
42
+ data = {
43
+ 'productid': range(1, num_products + 1),
44
+ 'productname': [f"Product {i}" for i in range(1, num_products + 1)],
45
+ 'category': np.random.choice(categories, num_products),
46
+ 'baseprice': np.random.uniform(5.0, 500.0, num_products).round(2),
47
+ 'instock': np.random.choice([True, False], num_products, p=[0.8, 0.2])
48
+ }
49
+
50
+ return pd.DataFrame(data)
@@ -0,0 +1,137 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ from datetime import datetime, timedelta
4
+ import os
5
+
6
+ # Set random seed for reproducibility
7
+ np.random.seed(42)
8
+
9
+ # Define output directory
10
+ OUTPUT_DIR = 'test_data'
11
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
12
+
13
+ def create_sales_data(num_records=1000):
14
+ # Generate dates for the last 365 days
15
+ end_date = datetime.now()
16
+ start_date = end_date - timedelta(days=365)
17
+ dates = [start_date + timedelta(days=x) for x in range(366)]
18
+ random_dates = np.random.choice(dates, num_records)
19
+
20
+ # Create product data
21
+ products = ['Laptop', 'Smartphone', 'Tablet', 'Monitor', 'Keyboard', 'Mouse', 'Headphones', 'Printer']
22
+ product_prices = {
23
+ 'Laptop': (800, 2000),
24
+ 'Smartphone': (400, 1200),
25
+ 'Tablet': (200, 800),
26
+ 'Monitor': (150, 500),
27
+ 'Keyboard': (20, 150),
28
+ 'Mouse': (10, 80),
29
+ 'Headphones': (30, 300),
30
+ 'Printer': (100, 400)
31
+ }
32
+
33
+ # Generate random data
34
+ data = {
35
+ 'OrderID': range(1, num_records + 1),
36
+ 'Date': random_dates,
37
+ 'ProductID': np.random.randint(1, len(products) + 1, num_records), # Changed to ProductID for joining
38
+ 'Quantity': np.random.randint(1, 11, num_records),
39
+ 'CustomerID': np.random.randint(1, 201, num_records),
40
+ 'Region': np.random.choice(['North', 'South', 'East', 'West'], num_records)
41
+ }
42
+
43
+ # Calculate prices based on product
44
+ product_list = [products[pid-1] for pid in data['ProductID']]
45
+ data['Price'] = [np.random.uniform(product_prices[p][0], product_prices[p][1])
46
+ for p in product_list]
47
+ data['TotalAmount'] = [price * qty for price, qty in zip(data['Price'], data['Quantity'])]
48
+
49
+ # Create DataFrame
50
+ df = pd.DataFrame(data)
51
+
52
+ # Round numerical columns
53
+ df['Price'] = df['Price'].round(2)
54
+ df['TotalAmount'] = df['TotalAmount'].round(2)
55
+
56
+ # Sort by Date
57
+ return df.sort_values('Date')
58
+
59
+ def create_customer_data(num_customers=200):
60
+ # Generate customer data
61
+ data = {
62
+ 'CustomerID': range(1, num_customers + 1),
63
+ 'FirstName': [f'Customer{i}' for i in range(1, num_customers + 1)],
64
+ 'LastName': [f'Lastname{i}' for i in range(1, num_customers + 1)],
65
+ 'Email': [f'customer{i}@example.com' for i in range(1, num_customers + 1)],
66
+ 'JoinDate': [datetime.now() - timedelta(days=np.random.randint(1, 1000))
67
+ for _ in range(num_customers)],
68
+ 'CustomerType': np.random.choice(['Regular', 'Premium', 'VIP'], num_customers),
69
+ 'CreditScore': np.random.randint(300, 851, num_customers)
70
+ }
71
+
72
+ return pd.DataFrame(data)
73
+
74
+ def create_product_data():
75
+ # Create detailed product information
76
+ products = {
77
+ 'ProductID': range(1, 9),
78
+ 'ProductName': ['Laptop', 'Smartphone', 'Tablet', 'Monitor', 'Keyboard', 'Mouse', 'Headphones', 'Printer'],
79
+ 'Category': ['Computers', 'Mobile', 'Mobile', 'Accessories', 'Accessories', 'Accessories', 'Audio', 'Peripherals'],
80
+ 'Brand': ['TechPro', 'MobileX', 'TabletCo', 'ViewMax', 'TypeMaster', 'ClickPro', 'SoundMax', 'PrintPro'],
81
+ 'StockQuantity': np.random.randint(50, 500, 8),
82
+ 'MinPrice': [800, 400, 200, 150, 20, 10, 30, 100],
83
+ 'MaxPrice': [2000, 1200, 800, 500, 150, 80, 300, 400],
84
+ 'Weight_kg': [2.5, 0.2, 0.5, 3.0, 0.8, 0.1, 0.3, 5.0],
85
+ 'WarrantyMonths': [24, 12, 12, 36, 12, 12, 24, 12]
86
+ }
87
+
88
+ return pd.DataFrame(products)
89
+
90
+ if __name__ == '__main__':
91
+ # Create and save sales data
92
+ sales_df = create_sales_data()
93
+ sales_output = os.path.join(OUTPUT_DIR, 'sample_sales_data.xlsx')
94
+ sales_df.to_excel(sales_output, index=False)
95
+ print(f"Created sales data in '{sales_output}'")
96
+ print(f"Number of sales records: {len(sales_df)}")
97
+
98
+ # Create and save customer data as parquet
99
+ customer_df = create_customer_data()
100
+ customer_output = os.path.join(OUTPUT_DIR, 'customer_data.parquet')
101
+ customer_df.to_parquet(customer_output, index=False)
102
+ print(f"\nCreated customer data in '{customer_output}'")
103
+ print(f"Number of customers: {len(customer_df)}")
104
+
105
+ # Create and save product data
106
+ product_df = create_product_data()
107
+ product_output = os.path.join(OUTPUT_DIR, 'product_catalog.xlsx')
108
+ product_df.to_excel(product_output, index=False)
109
+ print(f"\nCreated product catalog in '{product_output}'")
110
+ print(f"Number of products: {len(product_df)}")
111
+
112
+ # Print sample queries
113
+ print("\nSample SQL queries for joining the data:")
114
+ print("""
115
+ -- Join sales with customer data
116
+ SELECT s.*, c.FirstName, c.LastName, c.CustomerType
117
+ FROM test_data.sample_sales_data s
118
+ JOIN test_data.customer_data c ON s.CustomerID = c.CustomerID;
119
+
120
+ -- Join sales with product data
121
+ SELECT s.*, p.ProductName, p.Category, p.Brand
122
+ FROM test_data.sample_sales_data s
123
+ JOIN test_data.product_catalog p ON s.ProductID = p.ProductID;
124
+
125
+ -- Three-way join with aggregation
126
+ SELECT
127
+ p.Category,
128
+ c.CustomerType,
129
+ COUNT(*) as NumOrders,
130
+ SUM(s.TotalAmount) as TotalRevenue,
131
+ AVG(s.Quantity) as AvgQuantity
132
+ FROM test_data.sample_sales_data s
133
+ JOIN test_data.customer_data c ON s.CustomerID = c.CustomerID
134
+ JOIN test_data.product_catalog p ON s.ProductID = p.ProductID
135
+ GROUP BY p.Category, c.CustomerType
136
+ ORDER BY p.Category, c.CustomerType;
137
+ """)
sqlshell/editor.py ADDED
@@ -0,0 +1,355 @@
1
+ from PyQt6.QtWidgets import QPlainTextEdit, QWidget, QCompleter
2
+ from PyQt6.QtCore import Qt, QSize, QRect, QStringListModel
3
+ from PyQt6.QtGui import QFont, QColor, QTextCursor, QPainter, QBrush
4
+
5
+ class LineNumberArea(QWidget):
6
+ def __init__(self, editor):
7
+ super().__init__(editor)
8
+ self.editor = editor
9
+
10
+ def sizeHint(self):
11
+ return QSize(self.editor.line_number_area_width(), 0)
12
+
13
+ def paintEvent(self, event):
14
+ self.editor.line_number_area_paint_event(event)
15
+
16
+ class SQLEditor(QPlainTextEdit):
17
+ def __init__(self, parent=None):
18
+ super().__init__(parent)
19
+ self.line_number_area = LineNumberArea(self)
20
+
21
+ # Set monospaced font
22
+ font = QFont("Consolas", 12) # Increased font size for better readability
23
+ font.setFixedPitch(True)
24
+ self.setFont(font)
25
+
26
+ # Connect signals
27
+ self.blockCountChanged.connect(self.update_line_number_area_width)
28
+ self.updateRequest.connect(self.update_line_number_area)
29
+
30
+ # Initialize
31
+ self.update_line_number_area_width(0)
32
+
33
+ # Set tab width to 4 spaces
34
+ self.setTabStopDistance(4 * self.fontMetrics().horizontalAdvance(' '))
35
+
36
+ # Set placeholder text
37
+ self.setPlaceholderText("Enter your SQL query here...")
38
+
39
+ # Initialize completer
40
+ self.completer = None
41
+
42
+ # SQL Keywords for autocomplete
43
+ self.sql_keywords = [
44
+ "SELECT", "FROM", "WHERE", "AND", "OR", "INNER", "OUTER", "LEFT", "RIGHT", "JOIN",
45
+ "ON", "GROUP", "BY", "HAVING", "ORDER", "LIMIT", "OFFSET", "UNION", "EXCEPT", "INTERSECT",
46
+ "CREATE", "TABLE", "INDEX", "VIEW", "INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE",
47
+ "TRUNCATE", "ALTER", "ADD", "DROP", "COLUMN", "CONSTRAINT", "PRIMARY", "KEY", "FOREIGN", "REFERENCES",
48
+ "UNIQUE", "NOT", "NULL", "IS", "DISTINCT", "CASE", "WHEN", "THEN", "ELSE", "END",
49
+ "AS", "WITH", "BETWEEN", "LIKE", "IN", "EXISTS", "ALL", "ANY", "SOME", "DESC", "ASC",
50
+ "AVG", "COUNT", "SUM", "MAX", "MIN", "COALESCE", "CAST", "CONVERT"
51
+ ]
52
+
53
+ # Initialize with SQL keywords
54
+ self.set_completer(QCompleter(self.sql_keywords))
55
+
56
+ # Set modern selection color
57
+ self.selection_color = QColor("#3498DB")
58
+ self.selection_color.setAlpha(50) # Make it semi-transparent
59
+
60
+ def set_completer(self, completer):
61
+ """Set the completer for the editor"""
62
+ if self.completer:
63
+ self.completer.disconnect(self)
64
+
65
+ self.completer = completer
66
+
67
+ if not self.completer:
68
+ return
69
+
70
+ self.completer.setWidget(self)
71
+ self.completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
72
+ self.completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
73
+ self.completer.activated.connect(self.insert_completion)
74
+
75
+ def update_completer_model(self, words):
76
+ """Update the completer model with new words"""
77
+ if not self.completer:
78
+ return
79
+
80
+ # Combine SQL keywords with table/column names
81
+ all_words = self.sql_keywords + words
82
+
83
+ # Create a model with all words
84
+ model = QStringListModel()
85
+ model.setStringList(all_words)
86
+
87
+ # Set the model to the completer
88
+ self.completer.setModel(model)
89
+
90
+ def text_under_cursor(self):
91
+ """Get the text under the cursor for completion"""
92
+ tc = self.textCursor()
93
+ tc.select(QTextCursor.SelectionType.WordUnderCursor)
94
+ return tc.selectedText()
95
+
96
+ def insert_completion(self, completion):
97
+ """Insert the completion text"""
98
+ if self.completer.widget() != self:
99
+ return
100
+
101
+ tc = self.textCursor()
102
+ extra = len(completion) - len(self.completer.completionPrefix())
103
+ tc.movePosition(QTextCursor.MoveOperation.Left)
104
+ tc.movePosition(QTextCursor.MoveOperation.EndOfWord)
105
+ tc.insertText(completion[-extra:] + " ")
106
+ self.setTextCursor(tc)
107
+
108
+ def complete(self):
109
+ """Show completion popup"""
110
+ prefix = self.text_under_cursor()
111
+
112
+ if not prefix or len(prefix) < 2: # Only show completions for words with at least 2 characters
113
+ if self.completer.popup().isVisible():
114
+ self.completer.popup().hide()
115
+ return
116
+
117
+ self.completer.setCompletionPrefix(prefix)
118
+
119
+ # If no completions, hide popup
120
+ if self.completer.completionCount() == 0:
121
+ self.completer.popup().hide()
122
+ return
123
+
124
+ # Get popup and position it under the current text
125
+ popup = self.completer.popup()
126
+ popup.setCurrentIndex(self.completer.completionModel().index(0, 0))
127
+
128
+ # Calculate position for the popup
129
+ cr = self.cursorRect()
130
+ cr.setWidth(self.completer.popup().sizeHintForColumn(0) +
131
+ self.completer.popup().verticalScrollBar().sizeHint().width())
132
+
133
+ # Show the popup
134
+ self.completer.complete(cr)
135
+
136
+ def keyPressEvent(self, event):
137
+ # Handle completer popup navigation
138
+ if self.completer and self.completer.popup().isVisible():
139
+ # Handle navigation keys for the popup
140
+ if event.key() in [Qt.Key.Key_Enter, Qt.Key.Key_Return, Qt.Key.Key_Tab,
141
+ Qt.Key.Key_Escape, Qt.Key.Key_Up, Qt.Key.Key_Down]:
142
+ event.ignore()
143
+ return
144
+
145
+ # Handle special key combinations
146
+ if event.key() == Qt.Key.Key_Tab:
147
+ # Insert 4 spaces instead of a tab character
148
+ self.insertPlainText(" ")
149
+ return
150
+
151
+ # Auto-indentation for new lines
152
+ if event.key() in [Qt.Key.Key_Return, Qt.Key.Key_Enter]:
153
+ cursor = self.textCursor()
154
+ block = cursor.block()
155
+ text = block.text()
156
+
157
+ # Get the indentation of the current line
158
+ indentation = ""
159
+ for char in text:
160
+ if char.isspace():
161
+ indentation += char
162
+ else:
163
+ break
164
+
165
+ # Check if line ends with an opening bracket or keywords that should increase indentation
166
+ increase_indent = ""
167
+ if text.strip().endswith("(") or any(text.strip().upper().endswith(keyword) for keyword in
168
+ ["SELECT", "FROM", "WHERE", "GROUP BY", "ORDER BY", "HAVING"]):
169
+ increase_indent = " "
170
+
171
+ # Insert new line with proper indentation
172
+ super().keyPressEvent(event)
173
+ self.insertPlainText(indentation + increase_indent)
174
+ return
175
+
176
+ # Handle keyboard shortcuts
177
+ if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
178
+ if event.key() == Qt.Key.Key_Space:
179
+ # Show completion popup
180
+ self.complete()
181
+ return
182
+ elif event.key() == Qt.Key.Key_K:
183
+ # Comment/uncomment the selected lines
184
+ self.toggle_comment()
185
+ return
186
+
187
+ # For normal key presses
188
+ super().keyPressEvent(event)
189
+
190
+ # Check for autocomplete after typing
191
+ if event.text() and not event.text().isspace():
192
+ self.complete()
193
+
194
+ def paintEvent(self, event):
195
+ # Call the parent's paintEvent first
196
+ super().paintEvent(event)
197
+
198
+ # Get the current cursor
199
+ cursor = self.textCursor()
200
+
201
+ # If there's a selection, paint custom highlight
202
+ if cursor.hasSelection():
203
+ # Create a painter for this widget
204
+ painter = QPainter(self.viewport())
205
+
206
+ # Get the selection start and end positions
207
+ start = cursor.selectionStart()
208
+ end = cursor.selectionEnd()
209
+
210
+ # Create temporary cursor to get the rectangles
211
+ temp_cursor = QTextCursor(cursor)
212
+
213
+ # Move to start and get the starting position
214
+ temp_cursor.setPosition(start)
215
+ start_pos = self.cursorRect(temp_cursor)
216
+
217
+ # Move to end and get the ending position
218
+ temp_cursor.setPosition(end)
219
+ end_pos = self.cursorRect(temp_cursor)
220
+
221
+ # Set the highlight color with transparency
222
+ painter.setBrush(QBrush(self.selection_color))
223
+ painter.setPen(Qt.PenStyle.NoPen)
224
+
225
+ # Draw the highlight rectangle
226
+ if start_pos.top() == end_pos.top():
227
+ # Single line selection
228
+ painter.drawRect(QRect(start_pos.left(), start_pos.top(),
229
+ end_pos.right() - start_pos.left(), start_pos.height()))
230
+ else:
231
+ # Multi-line selection
232
+ # First line
233
+ painter.drawRect(QRect(start_pos.left(), start_pos.top(),
234
+ self.viewport().width() - start_pos.left(), start_pos.height()))
235
+
236
+ # Middle lines (if any)
237
+ if end_pos.top() > start_pos.top() + start_pos.height():
238
+ painter.drawRect(QRect(0, start_pos.top() + start_pos.height(),
239
+ self.viewport().width(),
240
+ end_pos.top() - (start_pos.top() + start_pos.height())))
241
+
242
+ # Last line
243
+ painter.drawRect(QRect(0, end_pos.top(), end_pos.right(), end_pos.height()))
244
+
245
+ painter.end()
246
+
247
+ def focusInEvent(self, event):
248
+ super().focusInEvent(event)
249
+ # Show temporary hint in status bar when editor gets focus
250
+ if hasattr(self.parent(), 'statusBar'):
251
+ self.parent().parent().parent().statusBar().showMessage('Press Ctrl+Space for autocomplete', 2000)
252
+
253
+ def toggle_comment(self):
254
+ cursor = self.textCursor()
255
+ if cursor.hasSelection():
256
+ # Get the selected text
257
+ start = cursor.selectionStart()
258
+ end = cursor.selectionEnd()
259
+
260
+ # Remember the selection
261
+ cursor.setPosition(start)
262
+ start_block = cursor.blockNumber()
263
+ cursor.setPosition(end)
264
+ end_block = cursor.blockNumber()
265
+
266
+ # Process each line in the selection
267
+ cursor.setPosition(start)
268
+ cursor.beginEditBlock()
269
+
270
+ for _ in range(start_block, end_block + 1):
271
+ # Move to start of line
272
+ cursor.movePosition(cursor.MoveOperation.StartOfLine)
273
+
274
+ # Check if the line is already commented
275
+ line_text = cursor.block().text().lstrip()
276
+ if line_text.startswith('--'):
277
+ # Remove comment
278
+ pos = cursor.block().text().find('--')
279
+ cursor.setPosition(cursor.block().position() + pos)
280
+ cursor.deleteChar()
281
+ cursor.deleteChar()
282
+ else:
283
+ # Add comment
284
+ cursor.insertText('--')
285
+
286
+ # Move to next line if not at the end
287
+ if not cursor.atEnd():
288
+ cursor.movePosition(cursor.MoveOperation.NextBlock)
289
+
290
+ cursor.endEditBlock()
291
+ else:
292
+ # Comment/uncomment current line
293
+ cursor.movePosition(cursor.MoveOperation.StartOfLine)
294
+ cursor.movePosition(cursor.MoveOperation.EndOfLine, cursor.MoveMode.KeepAnchor)
295
+ line_text = cursor.selectedText().lstrip()
296
+
297
+ cursor.movePosition(cursor.MoveOperation.StartOfLine)
298
+ if line_text.startswith('--'):
299
+ # Remove comment
300
+ pos = cursor.block().text().find('--')
301
+ cursor.setPosition(cursor.block().position() + pos)
302
+ cursor.deleteChar()
303
+ cursor.deleteChar()
304
+ else:
305
+ # Add comment
306
+ cursor.insertText('--')
307
+
308
+ def line_number_area_width(self):
309
+ digits = 1
310
+ max_num = max(1, self.blockCount())
311
+ while max_num >= 10:
312
+ max_num //= 10
313
+ digits += 1
314
+
315
+ space = 3 + self.fontMetrics().horizontalAdvance('9') * digits
316
+ return space
317
+
318
+ def update_line_number_area_width(self, _):
319
+ self.setViewportMargins(self.line_number_area_width(), 0, 0, 0)
320
+
321
+ def update_line_number_area(self, rect, dy):
322
+ if dy:
323
+ self.line_number_area.scroll(0, dy)
324
+ else:
325
+ self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height())
326
+
327
+ if rect.contains(self.viewport().rect()):
328
+ self.update_line_number_area_width(0)
329
+
330
+ def resizeEvent(self, event):
331
+ super().resizeEvent(event)
332
+ cr = self.contentsRect()
333
+ self.line_number_area.setGeometry(QRect(cr.left(), cr.top(), self.line_number_area_width(), cr.height()))
334
+
335
+ def line_number_area_paint_event(self, event):
336
+ painter = QPainter(self.line_number_area)
337
+ painter.fillRect(event.rect(), QColor("#f0f0f0")) # Light gray background
338
+
339
+ block = self.firstVisibleBlock()
340
+ block_number = block.blockNumber()
341
+ top = round(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
342
+ bottom = top + round(self.blockBoundingRect(block).height())
343
+
344
+ while block.isValid() and top <= event.rect().bottom():
345
+ if block.isVisible() and bottom >= event.rect().top():
346
+ number = str(block_number + 1)
347
+ painter.setPen(QColor("#808080")) # Gray text
348
+ painter.drawText(0, top, self.line_number_area.width() - 5,
349
+ self.fontMetrics().height(),
350
+ Qt.AlignmentFlag.AlignRight, number)
351
+
352
+ block = block.next()
353
+ top = bottom
354
+ bottom = top + round(self.blockBoundingRect(block).height())
355
+ block_number += 1