dataframe-textual 1.2.0__py3-none-any.whl → 1.3.9__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.
- dataframe_textual/common.py +33 -12
- dataframe_textual/data_frame_help_panel.py +6 -4
- dataframe_textual/data_frame_table.py +256 -172
- dataframe_textual/data_frame_viewer.py +19 -27
- dataframe_textual/sql_screen.py +202 -0
- dataframe_textual/table_screen.py +13 -17
- dataframe_textual/yes_no_screen.py +9 -5
- {dataframe_textual-1.2.0.dist-info → dataframe_textual-1.3.9.dist-info}/METADATA +46 -6
- dataframe_textual-1.3.9.dist-info/RECORD +14 -0
- {dataframe_textual-1.2.0.dist-info → dataframe_textual-1.3.9.dist-info}/entry_points.txt +1 -0
- dataframe_textual-1.2.0.dist-info/RECORD +0 -13
- {dataframe_textual-1.2.0.dist-info → dataframe_textual-1.3.9.dist-info}/WHEEL +0 -0
- {dataframe_textual-1.2.0.dist-info → dataframe_textual-1.3.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -9,6 +9,7 @@ from typing import Any
|
|
|
9
9
|
|
|
10
10
|
import polars as pl
|
|
11
11
|
from rich.text import Text
|
|
12
|
+
from textual import work
|
|
12
13
|
from textual.coordinate import Coordinate
|
|
13
14
|
from textual.events import Click
|
|
14
15
|
from textual.widgets import DataTable, TabPane
|
|
@@ -30,9 +31,11 @@ from .common import (
|
|
|
30
31
|
format_row,
|
|
31
32
|
get_next_item,
|
|
32
33
|
rindex,
|
|
34
|
+
sleep_async,
|
|
33
35
|
tentative_expr,
|
|
34
36
|
validate_expr,
|
|
35
37
|
)
|
|
38
|
+
from .sql_screen import AdvancedSqlScreen, SimpleSqlScreen
|
|
36
39
|
from .table_screen import FrequencyScreen, RowDetailScreen, StatisticsScreen
|
|
37
40
|
from .yes_no_screen import (
|
|
38
41
|
AddColumnScreen,
|
|
@@ -142,6 +145,10 @@ class DataFrameTable(DataTable):
|
|
|
142
145
|
- **"** - 📍 Filter to show only selected rows
|
|
143
146
|
- **T** - 🧹 Clear all selections and matches
|
|
144
147
|
|
|
148
|
+
## 🔍 SQL Interface
|
|
149
|
+
- **l** - 💬 Open simple SQL interface (select columns & WHERE clause)
|
|
150
|
+
- **L** - 🔎 Open advanced SQL interface (full SQL queries)
|
|
151
|
+
|
|
145
152
|
## ✏️ Edit & Modify
|
|
146
153
|
- **Double-click** - ✍️ Edit cell or rename column header
|
|
147
154
|
- **e** - ✍️ Edit current cell
|
|
@@ -154,11 +161,10 @@ class DataFrameTable(DataTable):
|
|
|
154
161
|
- **delete** - ❌ Clear current cell (set to NULL)
|
|
155
162
|
- **-** - ❌ Delete current column
|
|
156
163
|
- **_** - ❌ Delete column and those after
|
|
157
|
-
- **Ctrl
|
|
164
|
+
- **Ctrl+_** - ❌ Delete column and those before
|
|
158
165
|
- **d** - 📋 Duplicate current column
|
|
159
166
|
- **D** - 📋 Duplicate current row
|
|
160
167
|
|
|
161
|
-
|
|
162
168
|
## 🎯 Reorder
|
|
163
169
|
- **Shift+↑↓** - ⬆️⬇️ Move row up/down
|
|
164
170
|
- **Shift+←→** - ⬅️➡️ Move column left/right
|
|
@@ -235,7 +241,7 @@ class DataFrameTable(DataTable):
|
|
|
235
241
|
("delete", "clear_cell", "Clear cell"),
|
|
236
242
|
("minus", "delete_column", "Delete column"), # `-`
|
|
237
243
|
("underscore", "delete_column_and_after", "Delete column and those after"), # `_`
|
|
238
|
-
("ctrl+
|
|
244
|
+
("ctrl+underscore", "delete_column_and_before", "Delete column and those before"), # `Ctrl+_`
|
|
239
245
|
("x", "delete_row", "Delete row"),
|
|
240
246
|
("X", "delete_row_and_below", "Delete row and those below"),
|
|
241
247
|
("ctrl+x", "delete_row_and_up", "Delete row and those up"),
|
|
@@ -254,11 +260,14 @@ class DataFrameTable(DataTable):
|
|
|
254
260
|
("shift+up", "move_row_up", "Move row up"),
|
|
255
261
|
("shift+down", "move_row_down", "Move row down"),
|
|
256
262
|
# Type Conversion
|
|
257
|
-
("number_sign", "cast_column_dtype('
|
|
258
|
-
("percent_sign", "cast_column_dtype('
|
|
259
|
-
("exclamation_mark", "cast_column_dtype('
|
|
260
|
-
("dollar_sign", "cast_column_dtype('
|
|
263
|
+
("number_sign", "cast_column_dtype('pl.Int64')", "Cast column dtype to integer"), # `#`
|
|
264
|
+
("percent_sign", "cast_column_dtype('pl.Float64')", "Cast column dtype to float"), # `%`
|
|
265
|
+
("exclamation_mark", "cast_column_dtype('pl.Boolean')", "Cast column dtype to bool"), # `!`
|
|
266
|
+
("dollar_sign", "cast_column_dtype('pl.String')", "Cast column dtype to string"), # `$`
|
|
261
267
|
("at", "make_cell_clickable", "Make cell clickable"), # `@`
|
|
268
|
+
# Sql
|
|
269
|
+
("l", "simple_sql", "Simple SQL interface"),
|
|
270
|
+
("L", "advanced_sql", "Advanced SQL interface"),
|
|
262
271
|
# Undo/Redo
|
|
263
272
|
("u", "undo", "Undo"),
|
|
264
273
|
("U", "redo", "Redo"),
|
|
@@ -854,6 +863,14 @@ class DataFrameTable(DataTable):
|
|
|
854
863
|
"""Go to the previous selected row."""
|
|
855
864
|
self._previous_selected_row()
|
|
856
865
|
|
|
866
|
+
def action_simple_sql(self) -> None:
|
|
867
|
+
"""Open the SQL interface screen."""
|
|
868
|
+
self._simple_sql()
|
|
869
|
+
|
|
870
|
+
def action_advanced_sql(self) -> None:
|
|
871
|
+
"""Open the advanced SQL interface screen."""
|
|
872
|
+
self._advanced_sql()
|
|
873
|
+
|
|
857
874
|
def on_mouse_scroll_down(self, event) -> None:
|
|
858
875
|
"""Load more rows when scrolling down with mouse."""
|
|
859
876
|
self._check_and_load_more()
|
|
@@ -865,6 +882,9 @@ class DataFrameTable(DataTable):
|
|
|
865
882
|
Row keys are 0-based indices, which map directly to dataframe row indices.
|
|
866
883
|
Column keys are header names from the dataframe.
|
|
867
884
|
"""
|
|
885
|
+
self.loaded_rows = 0
|
|
886
|
+
self.show_row_labels = True
|
|
887
|
+
|
|
868
888
|
# Reset to original dataframe
|
|
869
889
|
if reset:
|
|
870
890
|
self.df = self.lazyframe.collect()
|
|
@@ -887,12 +907,15 @@ class DataFrameTable(DataTable):
|
|
|
887
907
|
stop = row_idx + 1
|
|
888
908
|
break
|
|
889
909
|
|
|
910
|
+
# Ensure all selected rows or matches are loaded
|
|
911
|
+
stop = max(stop, rindex(self.selected_rows, True) + 1)
|
|
912
|
+
stop = max(stop, max(self.matches.keys(), default=0) + 1)
|
|
913
|
+
|
|
890
914
|
# Save current cursor position before clearing
|
|
891
915
|
row_idx, col_idx = self.cursor_coordinate
|
|
892
916
|
|
|
893
917
|
self._setup_columns()
|
|
894
918
|
self._load_rows(stop)
|
|
895
|
-
self._do_highlight()
|
|
896
919
|
|
|
897
920
|
# Restore cursor position
|
|
898
921
|
if row_idx < len(self.rows) and col_idx < len(self.columns):
|
|
@@ -904,9 +927,7 @@ class DataFrameTable(DataTable):
|
|
|
904
927
|
Column keys are header names from the dataframe.
|
|
905
928
|
Column labels contain column names from the dataframe, with sort indicators if applicable.
|
|
906
929
|
"""
|
|
907
|
-
self.loaded_rows = 0
|
|
908
930
|
self.clear(columns=True)
|
|
909
|
-
self.show_row_labels = True
|
|
910
931
|
|
|
911
932
|
# Add columns with justified headers
|
|
912
933
|
for col, dtype in zip(self.df.columns, self.df.dtypes):
|
|
@@ -944,25 +965,33 @@ class DataFrameTable(DataTable):
|
|
|
944
965
|
start = self.loaded_rows
|
|
945
966
|
df_slice = self.df.slice(start, stop - start)
|
|
946
967
|
|
|
947
|
-
for
|
|
948
|
-
if not self.visible_rows[
|
|
968
|
+
for ridx, row in enumerate(df_slice.rows(), start):
|
|
969
|
+
if not self.visible_rows[ridx]:
|
|
949
970
|
continue # Skip hidden rows
|
|
950
971
|
|
|
951
|
-
|
|
972
|
+
is_selected = self.selected_rows[ridx]
|
|
973
|
+
match_cols = self.matches.get(ridx, set())
|
|
974
|
+
|
|
975
|
+
vals, dtypes, styles = [], [], []
|
|
952
976
|
for val, col, dtype in zip(row, self.df.columns, self.df.dtypes):
|
|
953
977
|
if col in self.hidden_columns:
|
|
954
978
|
continue # Skip hidden columns
|
|
979
|
+
|
|
955
980
|
vals.append(val)
|
|
956
981
|
dtypes.append(dtype)
|
|
957
|
-
|
|
982
|
+
# Highlight entire row if selected or has matches
|
|
983
|
+
styles.append("red" if is_selected or col in match_cols else None)
|
|
984
|
+
|
|
985
|
+
formatted_row = format_row(vals, dtypes, styles=styles, thousand_separator=self.thousand_separator)
|
|
958
986
|
|
|
959
987
|
# Always add labels so they can be shown/hidden via CSS
|
|
960
|
-
self.add_row(*formatted_row, key=str(
|
|
988
|
+
self.add_row(*formatted_row, key=str(ridx), label=str(ridx + 1))
|
|
961
989
|
|
|
962
990
|
# Update loaded rows count
|
|
963
991
|
self.loaded_rows = stop
|
|
964
992
|
|
|
965
993
|
# self.notify(f"Loaded [$accent]{stop}/{len(self.df)}[/] rows from [$success]{self.name}[/]", title="Load")
|
|
994
|
+
# self.log(f"Loaded {stop}/{len(self.df)} rows from {self.name}")
|
|
966
995
|
|
|
967
996
|
def _check_and_load_more(self) -> None:
|
|
968
997
|
"""Check if we need to load more rows and load them."""
|
|
@@ -1026,6 +1055,63 @@ class DataFrameTable(DataTable):
|
|
|
1026
1055
|
if need_update:
|
|
1027
1056
|
self.update_cell(row.key, col.key, cell_text)
|
|
1028
1057
|
|
|
1058
|
+
@work(exclusive=True, description="Loading rows asynchronously...")
|
|
1059
|
+
async def _load_rows_async(self, stop: int | None = None) -> None:
|
|
1060
|
+
"""Asynchronously load a batch of rows into the table.
|
|
1061
|
+
|
|
1062
|
+
Args:
|
|
1063
|
+
stop: Stop loading rows when this index is reached. If None, load until the end of the dataframe.
|
|
1064
|
+
"""
|
|
1065
|
+
if stop >= (total := len(self.df)):
|
|
1066
|
+
stop = total
|
|
1067
|
+
|
|
1068
|
+
if stop > self.loaded_rows:
|
|
1069
|
+
# Load incrementally with smaller chunks to prevent UI freezing
|
|
1070
|
+
chunk_size = min(100, stop - self.loaded_rows) # Load max 100 rows at a time
|
|
1071
|
+
next_stop = min(self.loaded_rows + chunk_size, stop)
|
|
1072
|
+
self._load_rows(next_stop)
|
|
1073
|
+
|
|
1074
|
+
# If there's more to load, schedule the next chunk with longer delay
|
|
1075
|
+
if next_stop < stop:
|
|
1076
|
+
# Use longer delay and call work method instead of set_timer
|
|
1077
|
+
await sleep_async(0.1) # 100ms delay to yield to UI
|
|
1078
|
+
self._load_rows_async(stop) # Recursive call within work context
|
|
1079
|
+
|
|
1080
|
+
# self.log(f"Async loaded {stop}/{len(self.df)} rows from {self.name}")
|
|
1081
|
+
|
|
1082
|
+
@work(exclusive=True, description="Doing highlight...")
|
|
1083
|
+
async def _do_highlight_async(self) -> None:
|
|
1084
|
+
"""Perform the highlighting preparation in a worker."""
|
|
1085
|
+
try:
|
|
1086
|
+
# Calculate what needs to be loaded without actually loading
|
|
1087
|
+
stop = rindex(self.selected_rows, True) + 1
|
|
1088
|
+
stop = max(stop, max(self.matches.keys(), default=0) + 1)
|
|
1089
|
+
|
|
1090
|
+
# Call the highlighting method (runs in background worker)
|
|
1091
|
+
self._highlight_async(stop)
|
|
1092
|
+
|
|
1093
|
+
except Exception as e:
|
|
1094
|
+
self.notify(f"Error preparing highlight: {str(e)}", title="Search", severity="error")
|
|
1095
|
+
|
|
1096
|
+
@work(exclusive=True, description="Highlighting matches...")
|
|
1097
|
+
async def _highlight_async(self, stop: int) -> None:
|
|
1098
|
+
"""Perform highlighting with async loading to avoid blocking."""
|
|
1099
|
+
# Load rows in smaller chunks to avoid blocking
|
|
1100
|
+
if stop > self.loaded_rows:
|
|
1101
|
+
# Load incrementally to avoid one big block
|
|
1102
|
+
chunk_size = min(100, stop - self.loaded_rows) # Load max 100 rows at a time
|
|
1103
|
+
next_stop = min(self.loaded_rows + chunk_size, stop)
|
|
1104
|
+
self._load_rows(next_stop)
|
|
1105
|
+
|
|
1106
|
+
# If there's more to load, yield to event loop with delay
|
|
1107
|
+
if next_stop < stop:
|
|
1108
|
+
await sleep_async(0.05) # 50ms delay to allow UI updates
|
|
1109
|
+
self._highlight_async(stop)
|
|
1110
|
+
return
|
|
1111
|
+
|
|
1112
|
+
# Now do the actual highlighting
|
|
1113
|
+
self._highlight_table(force=False)
|
|
1114
|
+
|
|
1029
1115
|
# History & Undo
|
|
1030
1116
|
def _create_history(self, description: str) -> None:
|
|
1031
1117
|
"""Create the initial history state."""
|
|
@@ -1080,16 +1166,16 @@ class DataFrameTable(DataTable):
|
|
|
1080
1166
|
self.notify("No actions to undo", title="Undo", severity="warning")
|
|
1081
1167
|
return
|
|
1082
1168
|
|
|
1083
|
-
# Save current state for redo
|
|
1084
|
-
self.history = self._create_history("Redo state")
|
|
1085
|
-
|
|
1086
1169
|
# Pop the last history state for undo
|
|
1087
1170
|
history = self.histories.pop()
|
|
1088
1171
|
|
|
1172
|
+
# Save current state for redo
|
|
1173
|
+
self.history = self._create_history(history.description)
|
|
1174
|
+
|
|
1089
1175
|
# Restore state
|
|
1090
1176
|
self._apply_history(history)
|
|
1091
1177
|
|
|
1092
|
-
|
|
1178
|
+
self.notify(f"Reverted: {history.description}", title="Undo")
|
|
1093
1179
|
|
|
1094
1180
|
def _redo(self) -> None:
|
|
1095
1181
|
"""Redo the last undone action."""
|
|
@@ -1097,8 +1183,10 @@ class DataFrameTable(DataTable):
|
|
|
1097
1183
|
self.notify("No actions to redo", title="Redo", severity="warning")
|
|
1098
1184
|
return
|
|
1099
1185
|
|
|
1186
|
+
description = self.history.description
|
|
1187
|
+
|
|
1100
1188
|
# Save current state for undo
|
|
1101
|
-
self._add_history(
|
|
1189
|
+
self._add_history(description)
|
|
1102
1190
|
|
|
1103
1191
|
# Restore state
|
|
1104
1192
|
self._apply_history(self.history)
|
|
@@ -1106,7 +1194,7 @@ class DataFrameTable(DataTable):
|
|
|
1106
1194
|
# Clear redo state
|
|
1107
1195
|
self.history = None
|
|
1108
1196
|
|
|
1109
|
-
|
|
1197
|
+
self.notify(f"Reapplied: {description}", title="Redo")
|
|
1110
1198
|
|
|
1111
1199
|
# View
|
|
1112
1200
|
def _view_row_detail(self) -> None:
|
|
@@ -1161,10 +1249,7 @@ class DataFrameTable(DataTable):
|
|
|
1161
1249
|
if fixed_columns >= 0:
|
|
1162
1250
|
self.fixed_columns = fixed_columns
|
|
1163
1251
|
|
|
1164
|
-
self.notify(
|
|
1165
|
-
f"Pinned [$accent]{fixed_rows}[/] rows and [$success]{fixed_columns}[/] columns",
|
|
1166
|
-
title="Pin",
|
|
1167
|
-
)
|
|
1252
|
+
# self.notify(f"Pinned [$accent]{fixed_rows}[/] rows and [$success]{fixed_columns}[/] columns", title="Pin")
|
|
1168
1253
|
|
|
1169
1254
|
# Delete & Move
|
|
1170
1255
|
def _delete_column(self, more: str = None) -> None:
|
|
@@ -1184,7 +1269,7 @@ class DataFrameTable(DataTable):
|
|
|
1184
1269
|
col_names_to_remove.append(col_key.value)
|
|
1185
1270
|
col_keys_to_remove.append(col_key)
|
|
1186
1271
|
|
|
1187
|
-
|
|
1272
|
+
message = f"Removed column [$success]{col_name}[/] and all columns before"
|
|
1188
1273
|
|
|
1189
1274
|
# Remove all columns after the current column
|
|
1190
1275
|
elif more == "after":
|
|
@@ -1193,16 +1278,16 @@ class DataFrameTable(DataTable):
|
|
|
1193
1278
|
col_names_to_remove.append(col_key.value)
|
|
1194
1279
|
col_keys_to_remove.append(col_key)
|
|
1195
1280
|
|
|
1196
|
-
|
|
1281
|
+
message = f"Removed column [$success]{col_name}[/] and all columns after"
|
|
1197
1282
|
|
|
1198
1283
|
# Remove only the current column
|
|
1199
1284
|
else:
|
|
1200
1285
|
col_names_to_remove.append(col_name)
|
|
1201
1286
|
col_keys_to_remove.append(col_key)
|
|
1202
|
-
|
|
1287
|
+
message = f"Removed column [$success]{col_name}[/]"
|
|
1203
1288
|
|
|
1204
1289
|
# Add to history
|
|
1205
|
-
self._add_history(
|
|
1290
|
+
self._add_history(message)
|
|
1206
1291
|
|
|
1207
1292
|
# Remove the columns from the table display using the column names as keys
|
|
1208
1293
|
for ck in col_keys_to_remove:
|
|
@@ -1229,7 +1314,7 @@ class DataFrameTable(DataTable):
|
|
|
1229
1314
|
# Remove from dataframe
|
|
1230
1315
|
self.df = self.df.drop(col_names_to_remove)
|
|
1231
1316
|
|
|
1232
|
-
|
|
1317
|
+
self.notify(message, title="Delete")
|
|
1233
1318
|
|
|
1234
1319
|
def _hide_column(self) -> None:
|
|
1235
1320
|
"""Hide the currently selected column from the table display."""
|
|
@@ -1275,7 +1360,7 @@ class DataFrameTable(DataTable):
|
|
|
1275
1360
|
self._setup_table()
|
|
1276
1361
|
|
|
1277
1362
|
self.notify(
|
|
1278
|
-
f"Showed [$accent]{hidden_row_count}[/] hidden row(s) and/or [$accent]{hidden_col_count}[/]
|
|
1363
|
+
f"Showed [$accent]{hidden_row_count}[/] hidden row(s) and/or [$accent]{hidden_col_count}[/] column(s)",
|
|
1279
1364
|
title="Show",
|
|
1280
1365
|
)
|
|
1281
1366
|
|
|
@@ -1317,10 +1402,7 @@ class DataFrameTable(DataTable):
|
|
|
1317
1402
|
# Move cursor to the new duplicated column
|
|
1318
1403
|
self.move_cursor(column=col_idx + 1)
|
|
1319
1404
|
|
|
1320
|
-
self.notify(
|
|
1321
|
-
f"Duplicated column [$accent]{col_name}[/] as [$success]{new_col_name}[/]",
|
|
1322
|
-
title="Duplicate",
|
|
1323
|
-
)
|
|
1405
|
+
# self.notify(f"Duplicated column [$accent]{col_name}[/] as [$success]{new_col_name}[/]", title="Duplicate")
|
|
1324
1406
|
|
|
1325
1407
|
def _delete_row(self, more: str = None) -> None:
|
|
1326
1408
|
"""Delete rows from the table and dataframe.
|
|
@@ -1385,7 +1467,7 @@ class DataFrameTable(DataTable):
|
|
|
1385
1467
|
self._setup_table()
|
|
1386
1468
|
|
|
1387
1469
|
deleted_count = old_count - len(self.df)
|
|
1388
|
-
if deleted_count >
|
|
1470
|
+
if deleted_count > 0:
|
|
1389
1471
|
self.notify(f"Deleted [$accent]{deleted_count}[/] row(s)", title="Delete")
|
|
1390
1472
|
|
|
1391
1473
|
def _duplicate_row(self) -> None:
|
|
@@ -1503,7 +1585,7 @@ class DataFrameTable(DataTable):
|
|
|
1503
1585
|
return
|
|
1504
1586
|
swap_idx = row_idx + 1
|
|
1505
1587
|
else:
|
|
1506
|
-
|
|
1588
|
+
# Invalid direction
|
|
1507
1589
|
return
|
|
1508
1590
|
|
|
1509
1591
|
row_key = self.coordinate_to_cell_key((row_idx, 0)).row_key
|
|
@@ -1653,7 +1735,7 @@ class DataFrameTable(DataTable):
|
|
|
1653
1735
|
col_key = col_name
|
|
1654
1736
|
self.update_cell(row_key, col_key, formatted_value, update_width=True)
|
|
1655
1737
|
|
|
1656
|
-
self.notify(f"Cell updated to [$success]{cell_value}[/]", title="Edit")
|
|
1738
|
+
# self.notify(f"Cell updated to [$success]{cell_value}[/]", title="Edit")
|
|
1657
1739
|
except Exception as e:
|
|
1658
1740
|
self.notify(f"Failed to update cell: {str(e)}", title="Edit", severity="error")
|
|
1659
1741
|
|
|
@@ -1682,7 +1764,7 @@ class DataFrameTable(DataTable):
|
|
|
1682
1764
|
# Check if term is a valid expression
|
|
1683
1765
|
elif tentative_expr(term):
|
|
1684
1766
|
try:
|
|
1685
|
-
expr = validate_expr(term, self.df, cidx)
|
|
1767
|
+
expr = validate_expr(term, self.df.columns, cidx)
|
|
1686
1768
|
except Exception as e:
|
|
1687
1769
|
self.notify(f"Error validating expression [$error]{term}[/]: {str(e)}", title="Edit", severity="error")
|
|
1688
1770
|
return
|
|
@@ -1695,7 +1777,7 @@ class DataFrameTable(DataTable):
|
|
|
1695
1777
|
expr = pl.lit(value)
|
|
1696
1778
|
except Exception:
|
|
1697
1779
|
self.notify(
|
|
1698
|
-
f"
|
|
1780
|
+
f"Error converting [$accent]{term}[/] to [$error]{dtype}[/]. Cast to string.",
|
|
1699
1781
|
title="Edit",
|
|
1700
1782
|
severity="error",
|
|
1701
1783
|
)
|
|
@@ -1708,16 +1790,13 @@ class DataFrameTable(DataTable):
|
|
|
1708
1790
|
# Apply the expression to the column
|
|
1709
1791
|
self.df = self.df.with_columns(expr.alias(col_name))
|
|
1710
1792
|
except Exception as e:
|
|
1711
|
-
self.notify(f"
|
|
1793
|
+
self.notify(f"Error applying expression: [$error]{str(e)}[/]", title="Edit", severity="error")
|
|
1712
1794
|
return
|
|
1713
1795
|
|
|
1714
1796
|
# Recreate the table for display
|
|
1715
1797
|
self._setup_table()
|
|
1716
1798
|
|
|
1717
|
-
self.notify(
|
|
1718
|
-
f"Column [$accent]{col_name}[/] updated with [$success]{expr}[/]",
|
|
1719
|
-
title="Edit",
|
|
1720
|
-
)
|
|
1799
|
+
# self.notify(f"Column [$accent]{col_name}[/] updated with [$success]{expr}[/]", title="Edit")
|
|
1721
1800
|
|
|
1722
1801
|
def _rename_column(self) -> None:
|
|
1723
1802
|
"""Open modal to rename the selected column."""
|
|
@@ -1764,10 +1843,7 @@ class DataFrameTable(DataTable):
|
|
|
1764
1843
|
# Move cursor to the renamed column
|
|
1765
1844
|
self.move_cursor(column=col_idx)
|
|
1766
1845
|
|
|
1767
|
-
self.notify(
|
|
1768
|
-
f"Renamed column [$success]{col_name}[/] to [$success]{new_name}[/]",
|
|
1769
|
-
title="Column",
|
|
1770
|
-
)
|
|
1846
|
+
# self.notify(f"Renamed column [$success]{col_name}[/] to [$success]{new_name}[/]", title="Column")
|
|
1771
1847
|
|
|
1772
1848
|
def _clear_cell(self) -> None:
|
|
1773
1849
|
"""Clear the current cell by setting its value to None."""
|
|
@@ -1795,9 +1871,9 @@ class DataFrameTable(DataTable):
|
|
|
1795
1871
|
|
|
1796
1872
|
self.update_cell(row_key, col_key, formatted_value)
|
|
1797
1873
|
|
|
1798
|
-
self.notify(f"Cell cleared to [$success]{NULL_DISPLAY}[/]", title="Clear")
|
|
1874
|
+
# self.notify(f"Cell cleared to [$success]{NULL_DISPLAY}[/]", title="Clear")
|
|
1799
1875
|
except Exception as e:
|
|
1800
|
-
self.notify(f"
|
|
1876
|
+
self.notify(f"Error clearing cell: {str(e)}", title="Clear", severity="error")
|
|
1801
1877
|
raise e
|
|
1802
1878
|
|
|
1803
1879
|
def _add_column(self, col_name: str = None, col_value: pl.Expr = None) -> None:
|
|
@@ -1840,9 +1916,9 @@ class DataFrameTable(DataTable):
|
|
|
1840
1916
|
# Move cursor to the new column
|
|
1841
1917
|
self.move_cursor(column=cidx + 1)
|
|
1842
1918
|
|
|
1843
|
-
self.notify(f"Added column [$success]{new_name}[/]", title="Add Column")
|
|
1919
|
+
# self.notify(f"Added column [$success]{new_name}[/]", title="Add Column")
|
|
1844
1920
|
except Exception as e:
|
|
1845
|
-
self.notify(f"
|
|
1921
|
+
self.notify(f"Error adding column: {str(e)}", title="Add Column", severity="error")
|
|
1846
1922
|
raise e
|
|
1847
1923
|
|
|
1848
1924
|
def _add_column_expr(self) -> None:
|
|
@@ -1884,53 +1960,32 @@ class DataFrameTable(DataTable):
|
|
|
1884
1960
|
|
|
1885
1961
|
# self.notify(f"Added column [$success]{col_name}[/]", title="Add Column")
|
|
1886
1962
|
except Exception as e:
|
|
1887
|
-
self.notify(f"
|
|
1963
|
+
self.notify(f"Error adding column: [$error]{str(e)}[/]", title="Add Column", severity="error")
|
|
1888
1964
|
raise e
|
|
1889
1965
|
|
|
1890
|
-
def
|
|
1891
|
-
"""Convert string type name to Polars DataType.
|
|
1892
|
-
|
|
1893
|
-
Args:
|
|
1894
|
-
dtype_str: String representation of the type ("string", "int", "float", "bool")
|
|
1895
|
-
|
|
1896
|
-
Returns:
|
|
1897
|
-
Corresponding Polars DataType
|
|
1898
|
-
|
|
1899
|
-
Raises:
|
|
1900
|
-
ValueError: If the type string is not recognized
|
|
1901
|
-
"""
|
|
1902
|
-
dtype_map = {
|
|
1903
|
-
"string": pl.String,
|
|
1904
|
-
"int": pl.Int64,
|
|
1905
|
-
"float": pl.Float64,
|
|
1906
|
-
"bool": pl.Boolean,
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
dtype_lower = dtype_str.lower().strip()
|
|
1910
|
-
return dtype_map.get(dtype_lower)
|
|
1911
|
-
|
|
1912
|
-
def _cast_column_dtype(self, dtype: str | pl.DataType = pl.String) -> None:
|
|
1966
|
+
def _cast_column_dtype(self, dtype: str) -> None:
|
|
1913
1967
|
"""Cast the current column to a different data type.
|
|
1914
1968
|
|
|
1915
1969
|
Args:
|
|
1916
|
-
dtype: Target data type (string
|
|
1970
|
+
dtype: Target data type (string representation, e.g., "pl.String", "pl.Int64")
|
|
1917
1971
|
"""
|
|
1918
1972
|
cidx = self.cursor_col_idx
|
|
1919
1973
|
col_name = self.cursor_col_name
|
|
1920
1974
|
current_dtype = self.df.dtypes[cidx]
|
|
1921
1975
|
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1976
|
+
try:
|
|
1977
|
+
target_dtype = eval(dtype)
|
|
1978
|
+
except Exception:
|
|
1979
|
+
self.notify(f"Invalid target data type: [$error]{dtype}[/]", title="Cast", severity="error")
|
|
1980
|
+
return
|
|
1981
|
+
|
|
1982
|
+
if current_dtype == target_dtype:
|
|
1983
|
+
self.notify(
|
|
1984
|
+
f"Column [$accent]{col_name}[/] is already of type [$success]{target_dtype}[/]",
|
|
1985
|
+
title="Cast",
|
|
1986
|
+
severity="warning",
|
|
1987
|
+
)
|
|
1988
|
+
return # No change needed
|
|
1934
1989
|
|
|
1935
1990
|
# Add to history
|
|
1936
1991
|
self._add_history(
|
|
@@ -1944,13 +1999,13 @@ class DataFrameTable(DataTable):
|
|
|
1944
1999
|
# Recreate the table display
|
|
1945
2000
|
self._setup_table()
|
|
1946
2001
|
|
|
2002
|
+
self.notify(f"Cast column [$accent]{col_name}[/] to [$success]{target_dtype}[/]", title="Cast")
|
|
2003
|
+
except Exception as e:
|
|
1947
2004
|
self.notify(
|
|
1948
|
-
f"
|
|
2005
|
+
f"Error casting column [$accent]{col_name}[/] to [$success]{target_dtype}[/]: {str(e)}",
|
|
1949
2006
|
title="Cast",
|
|
2007
|
+
severity="error",
|
|
1950
2008
|
)
|
|
1951
|
-
except Exception as e:
|
|
1952
|
-
self.notify(f"Failed to cast column: {str(e)}", title="Cast", severity="error")
|
|
1953
|
-
raise e
|
|
1954
2009
|
|
|
1955
2010
|
def _search_cursor_value(self) -> None:
|
|
1956
2011
|
"""Search with cursor value in current column."""
|
|
@@ -1959,7 +2014,7 @@ class DataFrameTable(DataTable):
|
|
|
1959
2014
|
# Get the value of the currently selected cell
|
|
1960
2015
|
term = NULL if self.cursor_value is None else str(self.cursor_value)
|
|
1961
2016
|
|
|
1962
|
-
self._do_search((term, cidx, False,
|
|
2017
|
+
self._do_search((term, cidx, False, True))
|
|
1963
2018
|
|
|
1964
2019
|
def _search_expr(self) -> None:
|
|
1965
2020
|
"""Search by expression."""
|
|
@@ -1978,6 +2033,7 @@ class DataFrameTable(DataTable):
|
|
|
1978
2033
|
"""Search for a term."""
|
|
1979
2034
|
if result is None:
|
|
1980
2035
|
return
|
|
2036
|
+
|
|
1981
2037
|
term, cidx, match_nocase, match_whole = result
|
|
1982
2038
|
col_name = self.df.columns[cidx]
|
|
1983
2039
|
|
|
@@ -1987,12 +2043,10 @@ class DataFrameTable(DataTable):
|
|
|
1987
2043
|
# Support for polars expressions
|
|
1988
2044
|
elif tentative_expr(term):
|
|
1989
2045
|
try:
|
|
1990
|
-
expr = validate_expr(term, self.df, cidx)
|
|
2046
|
+
expr = validate_expr(term, self.df.columns, cidx)
|
|
1991
2047
|
except Exception as e:
|
|
1992
2048
|
self.notify(
|
|
1993
|
-
f"
|
|
1994
|
-
title="Search",
|
|
1995
|
-
severity="error",
|
|
2049
|
+
f"Error validating expression [$error]{term}[/]: {str(e)}", title="Search", severity="error"
|
|
1996
2050
|
)
|
|
1997
2051
|
return
|
|
1998
2052
|
|
|
@@ -2016,7 +2070,7 @@ class DataFrameTable(DataTable):
|
|
|
2016
2070
|
term = f"(?i){term}"
|
|
2017
2071
|
expr = pl.col(col_name).cast(pl.String).str.contains(term)
|
|
2018
2072
|
self.notify(
|
|
2019
|
-
f"
|
|
2073
|
+
f"Error converting [$accent]{term}[/] to [$error]{dtype}[/]. Cast to string.",
|
|
2020
2074
|
title="Search",
|
|
2021
2075
|
severity="warning",
|
|
2022
2076
|
)
|
|
@@ -2031,7 +2085,7 @@ class DataFrameTable(DataTable):
|
|
|
2031
2085
|
matches = set(lf.filter(expr).select(RIDX).collect().to_series().to_list())
|
|
2032
2086
|
except Exception as e:
|
|
2033
2087
|
self.notify(
|
|
2034
|
-
f"Error applying search filter: [$error]{str(e)}[/]",
|
|
2088
|
+
f"Error applying search filter [$accent]{term}[/]: [$error]{str(e)}[/]",
|
|
2035
2089
|
title="Search",
|
|
2036
2090
|
severity="error",
|
|
2037
2091
|
)
|
|
@@ -2040,7 +2094,7 @@ class DataFrameTable(DataTable):
|
|
|
2040
2094
|
match_count = len(matches)
|
|
2041
2095
|
if match_count == 0:
|
|
2042
2096
|
self.notify(
|
|
2043
|
-
f"No matches found for [$
|
|
2097
|
+
f"No matches found for [$accent]{term}[/]. Try [$warning](?i)abc[/] for case-insensitive search.",
|
|
2044
2098
|
title="Search",
|
|
2045
2099
|
severity="warning",
|
|
2046
2100
|
)
|
|
@@ -2053,11 +2107,12 @@ class DataFrameTable(DataTable):
|
|
|
2053
2107
|
for m in matches:
|
|
2054
2108
|
self.selected_rows[m] = True
|
|
2055
2109
|
|
|
2056
|
-
#
|
|
2057
|
-
self._do_highlight()
|
|
2058
|
-
|
|
2110
|
+
# Show notification immediately, then start highlighting
|
|
2059
2111
|
self.notify(f"Found [$accent]{match_count}[/] matches for [$success]{term}[/]", title="Search")
|
|
2060
2112
|
|
|
2113
|
+
# Start highlighting in a worker to avoid blocking the UI
|
|
2114
|
+
self._do_highlight_async()
|
|
2115
|
+
|
|
2061
2116
|
def _find_matches(
|
|
2062
2117
|
self, term: str, cidx: int | None = None, match_nocase: bool = False, match_whole: bool = False
|
|
2063
2118
|
) -> dict[int, set[int]]:
|
|
@@ -2095,7 +2150,7 @@ class DataFrameTable(DataTable):
|
|
|
2095
2150
|
expr = pl.col(col_name).is_null()
|
|
2096
2151
|
elif tentative_expr(term):
|
|
2097
2152
|
try:
|
|
2098
|
-
expr = validate_expr(term, self.df, col_idx)
|
|
2153
|
+
expr = validate_expr(term, self.df.columns, col_idx)
|
|
2099
2154
|
except Exception as e:
|
|
2100
2155
|
raise Exception(f"Error validating Polars expression: {str(e)}")
|
|
2101
2156
|
else:
|
|
@@ -2127,9 +2182,9 @@ class DataFrameTable(DataTable):
|
|
|
2127
2182
|
|
|
2128
2183
|
if scope == "column":
|
|
2129
2184
|
cidx = self.cursor_col_idx
|
|
2130
|
-
self._do_find((term, cidx, False,
|
|
2185
|
+
self._do_find((term, cidx, False, True))
|
|
2131
2186
|
else:
|
|
2132
|
-
self._do_find_global((term, None, False,
|
|
2187
|
+
self._do_find_global((term, None, False, True))
|
|
2133
2188
|
|
|
2134
2189
|
def _find_expr(self, scope="column") -> None:
|
|
2135
2190
|
"""Open screen to find by expression.
|
|
@@ -2158,16 +2213,12 @@ class DataFrameTable(DataTable):
|
|
|
2158
2213
|
try:
|
|
2159
2214
|
matches = self._find_matches(term, cidx, match_nocase, match_whole)
|
|
2160
2215
|
except Exception as e:
|
|
2161
|
-
self.notify(
|
|
2162
|
-
f"Error finding matches: [$error]{str(e)}[/]",
|
|
2163
|
-
title="Find",
|
|
2164
|
-
severity="error",
|
|
2165
|
-
)
|
|
2216
|
+
self.notify(f"Error finding matches for [$error]{term}[/]: {str(e)}", title="Find", severity="error")
|
|
2166
2217
|
return
|
|
2167
2218
|
|
|
2168
2219
|
if not matches:
|
|
2169
2220
|
self.notify(
|
|
2170
|
-
f"No matches found for [$
|
|
2221
|
+
f"No matches found for [$accent]{term}[/] in current column. Try [$warning](?i)abc[/] for case-insensitive search.",
|
|
2171
2222
|
title="Find",
|
|
2172
2223
|
severity="warning",
|
|
2173
2224
|
)
|
|
@@ -2181,11 +2232,11 @@ class DataFrameTable(DataTable):
|
|
|
2181
2232
|
for ridx, col_idxs in matches.items():
|
|
2182
2233
|
self.matches[ridx].update(col_idxs)
|
|
2183
2234
|
|
|
2184
|
-
# Highlight matches
|
|
2185
|
-
self._do_highlight()
|
|
2186
|
-
|
|
2187
2235
|
self.notify(f"Found [$accent]{match_count}[/] matches for [$success]{term}[/]", title="Find")
|
|
2188
2236
|
|
|
2237
|
+
# Start highlighting in a worker to avoid blocking the UI
|
|
2238
|
+
self._do_highlight_async()
|
|
2239
|
+
|
|
2189
2240
|
def _do_find_global(self, result) -> None:
|
|
2190
2241
|
"""Global find a term across all columns."""
|
|
2191
2242
|
if result is None:
|
|
@@ -2195,16 +2246,12 @@ class DataFrameTable(DataTable):
|
|
|
2195
2246
|
try:
|
|
2196
2247
|
matches = self._find_matches(term, cidx=None, match_nocase=match_nocase, match_whole=match_whole)
|
|
2197
2248
|
except Exception as e:
|
|
2198
|
-
self.notify(
|
|
2199
|
-
f"Error finding matches: [$error]{str(e)}[/]",
|
|
2200
|
-
title="Find",
|
|
2201
|
-
severity="error",
|
|
2202
|
-
)
|
|
2249
|
+
self.notify(f"Error finding matches for [$error]{term}[/]: {str(e)}", title="Find", severity="error")
|
|
2203
2250
|
return
|
|
2204
2251
|
|
|
2205
2252
|
if not matches:
|
|
2206
2253
|
self.notify(
|
|
2207
|
-
f"No matches found for [$
|
|
2254
|
+
f"No matches found for [$accent]{term}[/] in any column. Try [$warning](?i)abc[/] for case-insensitive search.",
|
|
2208
2255
|
title="Global Find",
|
|
2209
2256
|
severity="warning",
|
|
2210
2257
|
)
|
|
@@ -2218,14 +2265,13 @@ class DataFrameTable(DataTable):
|
|
|
2218
2265
|
for ridx, col_idxs in matches.items():
|
|
2219
2266
|
self.matches[ridx].update(col_idxs)
|
|
2220
2267
|
|
|
2221
|
-
# Highlight matches
|
|
2222
|
-
self._do_highlight()
|
|
2223
|
-
|
|
2224
2268
|
self.notify(
|
|
2225
|
-
f"Found [$accent]{match_count}[/] matches for [$success]{term}[/] across all columns",
|
|
2226
|
-
title="Global Find",
|
|
2269
|
+
f"Found [$accent]{match_count}[/] matches for [$success]{term}[/] across all columns", title="Global Find"
|
|
2227
2270
|
)
|
|
2228
2271
|
|
|
2272
|
+
# Start highlighting in a worker to avoid blocking the UI
|
|
2273
|
+
self._do_highlight_async()
|
|
2274
|
+
|
|
2229
2275
|
def _next_match(self) -> None:
|
|
2230
2276
|
"""Move cursor to the next match."""
|
|
2231
2277
|
if not self.matches:
|
|
@@ -2364,16 +2410,12 @@ class DataFrameTable(DataTable):
|
|
|
2364
2410
|
matches = self._find_matches(term_find, cidx, match_nocase, match_whole)
|
|
2365
2411
|
|
|
2366
2412
|
if not matches:
|
|
2367
|
-
self.notify(
|
|
2368
|
-
f"No matches found for [$warning]{term_find}[/]",
|
|
2369
|
-
title="Replace",
|
|
2370
|
-
severity="warning",
|
|
2371
|
-
)
|
|
2413
|
+
self.notify(f"No matches found for [$warning]{term_find}[/]", title="Replace", severity="warning")
|
|
2372
2414
|
return
|
|
2373
2415
|
|
|
2374
2416
|
# Add to history
|
|
2375
2417
|
self._add_history(
|
|
2376
|
-
f"
|
|
2418
|
+
f"Replaced [$accent]{term_find}[/] with [$success]{term_replace}[/] in column [$accent]{col_name}[/]"
|
|
2377
2419
|
)
|
|
2378
2420
|
|
|
2379
2421
|
# Update matches
|
|
@@ -2596,10 +2638,7 @@ class DataFrameTable(DataTable):
|
|
|
2596
2638
|
|
|
2597
2639
|
# Check if we're highlighting or un-highlighting
|
|
2598
2640
|
if new_selected_count := self.selected_rows.count(True):
|
|
2599
|
-
self.notify(
|
|
2600
|
-
f"Toggled selection for [$accent]{new_selected_count}[/] rows",
|
|
2601
|
-
title="Toggle",
|
|
2602
|
-
)
|
|
2641
|
+
self.notify(f"Toggled selection for [$accent]{new_selected_count}[/] rows", title="Toggle")
|
|
2603
2642
|
|
|
2604
2643
|
# Refresh the highlighting
|
|
2605
2644
|
self._do_highlight(force=True)
|
|
@@ -2665,15 +2704,12 @@ class DataFrameTable(DataTable):
|
|
|
2665
2704
|
# Recreate the table for display
|
|
2666
2705
|
self._setup_table()
|
|
2667
2706
|
|
|
2668
|
-
self.notify(
|
|
2669
|
-
f"Removed unselected rows. Now showing [$accent]{selected_count}[/] rows",
|
|
2670
|
-
title="Filter",
|
|
2671
|
-
)
|
|
2707
|
+
self.notify(f"Removed unselected rows. Now showing [$accent]{selected_count}[/] rows", title="Filter")
|
|
2672
2708
|
|
|
2673
2709
|
def _view_rows(self) -> None:
|
|
2674
2710
|
"""View rows.
|
|
2675
2711
|
|
|
2676
|
-
If there are selected rows, view those rows.
|
|
2712
|
+
If there are selected rows or matches, view those rows.
|
|
2677
2713
|
Otherwise, view based on the value of the currently selected cell.
|
|
2678
2714
|
"""
|
|
2679
2715
|
|
|
@@ -2689,7 +2725,7 @@ class DataFrameTable(DataTable):
|
|
|
2689
2725
|
ridx = self.cursor_row_idx
|
|
2690
2726
|
term = str(self.df.item(ridx, cidx))
|
|
2691
2727
|
|
|
2692
|
-
self._do_view_rows((term, cidx, False,
|
|
2728
|
+
self._do_view_rows((term, cidx, False, True))
|
|
2693
2729
|
|
|
2694
2730
|
def _view_rows_expr(self) -> None:
|
|
2695
2731
|
"""Open the filter screen to enter an expression."""
|
|
@@ -2718,10 +2754,10 @@ class DataFrameTable(DataTable):
|
|
|
2718
2754
|
elif tentative_expr(term):
|
|
2719
2755
|
# Support for polars expressions
|
|
2720
2756
|
try:
|
|
2721
|
-
expr = validate_expr(term, self.df, cidx)
|
|
2757
|
+
expr = validate_expr(term, self.df.columns, cidx)
|
|
2722
2758
|
except Exception as e:
|
|
2723
2759
|
self.notify(
|
|
2724
|
-
f"Error validating
|
|
2760
|
+
f"Error validating expression [$error]{term}[/]: {str(e)}", title="Filter", severity="error"
|
|
2725
2761
|
)
|
|
2726
2762
|
return
|
|
2727
2763
|
else:
|
|
@@ -2743,9 +2779,7 @@ class DataFrameTable(DataTable):
|
|
|
2743
2779
|
term = f"(?i){term}"
|
|
2744
2780
|
expr = pl.col(col_name).cast(pl.String).str.contains(term)
|
|
2745
2781
|
self.notify(
|
|
2746
|
-
f"Unknown column type [$warning]{dtype}[/]. Cast to string.",
|
|
2747
|
-
title="Filter",
|
|
2748
|
-
severity="warning",
|
|
2782
|
+
f"Unknown column type [$warning]{dtype}[/]. Cast to string.", title="Filter", severity="warning"
|
|
2749
2783
|
)
|
|
2750
2784
|
|
|
2751
2785
|
# Lazyframe with row indices
|
|
@@ -2759,17 +2793,13 @@ class DataFrameTable(DataTable):
|
|
|
2759
2793
|
try:
|
|
2760
2794
|
df_filtered = lf.filter(expr).collect()
|
|
2761
2795
|
except Exception as e:
|
|
2762
|
-
self.notify(f"
|
|
2796
|
+
self.notify(f"Error applying filter [$error]{expr}[/]: {str(e)}", title="Filter", severity="error")
|
|
2763
2797
|
self.histories.pop() # Remove last history entry
|
|
2764
2798
|
return
|
|
2765
2799
|
|
|
2766
2800
|
matched_count = len(df_filtered)
|
|
2767
2801
|
if not matched_count:
|
|
2768
|
-
self.notify(
|
|
2769
|
-
f"No rows match the expression: [$success]{expr}[/]",
|
|
2770
|
-
title="Filter",
|
|
2771
|
-
severity="warning",
|
|
2772
|
-
)
|
|
2802
|
+
self.notify(f"No rows match the expression: [$success]{expr}[/]", title="Filter", severity="warning")
|
|
2773
2803
|
return
|
|
2774
2804
|
|
|
2775
2805
|
# Add to history
|
|
@@ -2784,11 +2814,9 @@ class DataFrameTable(DataTable):
|
|
|
2784
2814
|
|
|
2785
2815
|
# Recreate the table for display
|
|
2786
2816
|
self._setup_table()
|
|
2817
|
+
self._do_highlight()
|
|
2787
2818
|
|
|
2788
|
-
self.notify(
|
|
2789
|
-
f"Filtered to [$accent]{matched_count}[/] matching rows",
|
|
2790
|
-
title="Filter",
|
|
2791
|
-
)
|
|
2819
|
+
self.notify(f"Filtered to [$accent]{matched_count}[/] matching rows", title="Filter")
|
|
2792
2820
|
|
|
2793
2821
|
def _cycle_cursor_type(self) -> None:
|
|
2794
2822
|
"""Cycle through cursor types: cell -> row -> column -> cell."""
|
|
@@ -2862,6 +2890,9 @@ class DataFrameTable(DataTable):
|
|
|
2862
2890
|
filepath = Path(filename)
|
|
2863
2891
|
ext = filepath.suffix.lower()
|
|
2864
2892
|
|
|
2893
|
+
# Add to history
|
|
2894
|
+
self._add_history(f"Saved dataframe to [$success]{filename}[/]")
|
|
2895
|
+
|
|
2865
2896
|
try:
|
|
2866
2897
|
if ext in (".xlsx", ".xls"):
|
|
2867
2898
|
self._do_save_excel(filename)
|
|
@@ -2878,12 +2909,9 @@ class DataFrameTable(DataTable):
|
|
|
2878
2909
|
self.filename = filename # Update current filename
|
|
2879
2910
|
if not self._all_tabs:
|
|
2880
2911
|
extra = "current tab with " if len(self.app.tabs) > 1 else ""
|
|
2881
|
-
self.notify(
|
|
2882
|
-
f"Saved {extra}[$accent]{len(self.df)}[/] rows to [$success]{filename}[/]",
|
|
2883
|
-
title="Save",
|
|
2884
|
-
)
|
|
2912
|
+
self.notify(f"Saved {extra}[$accent]{len(self.df)}[/] rows to [$success]{filename}[/]", title="Save")
|
|
2885
2913
|
except Exception as e:
|
|
2886
|
-
self.notify(f"
|
|
2914
|
+
self.notify(f"Error saving [$error]{filename}[/]: {str(e)}", title="Save", severity="error")
|
|
2887
2915
|
raise e
|
|
2888
2916
|
|
|
2889
2917
|
def _do_save_excel(self, filename: str) -> None:
|
|
@@ -2903,14 +2931,10 @@ class DataFrameTable(DataTable):
|
|
|
2903
2931
|
|
|
2904
2932
|
# From ConfirmScreen callback, so notify accordingly
|
|
2905
2933
|
if self._all_tabs is True:
|
|
2906
|
-
self.notify(
|
|
2907
|
-
f"Saved all tabs to [$success]{filename}[/]",
|
|
2908
|
-
title="Save",
|
|
2909
|
-
)
|
|
2934
|
+
self.notify(f"Saved all tabs to [$success]{filename}[/]", title="Save")
|
|
2910
2935
|
else:
|
|
2911
2936
|
self.notify(
|
|
2912
|
-
f"Saved current tab with [$accent]{len(self.df)}[/] rows to [$success]{filename}[/]",
|
|
2913
|
-
title="Save",
|
|
2937
|
+
f"Saved current tab with [$accent]{len(self.df)}[/] rows to [$success]{filename}[/]", title="Save"
|
|
2914
2938
|
)
|
|
2915
2939
|
|
|
2916
2940
|
def _make_cell_clickable(self) -> None:
|
|
@@ -2944,6 +2968,66 @@ class DataFrameTable(DataTable):
|
|
|
2944
2968
|
|
|
2945
2969
|
if url_count:
|
|
2946
2970
|
self.notify(
|
|
2947
|
-
f"
|
|
2948
|
-
title="Make Clickable",
|
|
2971
|
+
f"Use Ctrl/Cmd click to open the links in column [$success]{col_key.value}[/]", title="Hyperlink"
|
|
2949
2972
|
)
|
|
2973
|
+
|
|
2974
|
+
def _simple_sql(self) -> None:
|
|
2975
|
+
"""Open the SQL interface screen."""
|
|
2976
|
+
self.app.push_screen(
|
|
2977
|
+
SimpleSqlScreen(self),
|
|
2978
|
+
callback=self._do_simple_sql,
|
|
2979
|
+
)
|
|
2980
|
+
|
|
2981
|
+
def _do_simple_sql(self, result) -> None:
|
|
2982
|
+
"""Handle SQL result result from SimpleSqlScreen."""
|
|
2983
|
+
if result is None:
|
|
2984
|
+
return
|
|
2985
|
+
columns, where = result
|
|
2986
|
+
|
|
2987
|
+
sql = f"SELECT {columns} FROM self"
|
|
2988
|
+
if where:
|
|
2989
|
+
sql += f" WHERE {where}"
|
|
2990
|
+
|
|
2991
|
+
self._do_sql(sql)
|
|
2992
|
+
|
|
2993
|
+
def _advanced_sql(self) -> None:
|
|
2994
|
+
"""Open the advanced SQL interface screen."""
|
|
2995
|
+
self.app.push_screen(
|
|
2996
|
+
AdvancedSqlScreen(self),
|
|
2997
|
+
callback=self._do_advanced_sql,
|
|
2998
|
+
)
|
|
2999
|
+
|
|
3000
|
+
def _do_advanced_sql(self, result) -> None:
|
|
3001
|
+
"""Handle SQL result result from AdvancedSqlScreen."""
|
|
3002
|
+
if result is None:
|
|
3003
|
+
return
|
|
3004
|
+
|
|
3005
|
+
self._do_sql(result)
|
|
3006
|
+
|
|
3007
|
+
def _do_sql(self, sql: str) -> None:
|
|
3008
|
+
"""Execute a SQL query directly.
|
|
3009
|
+
|
|
3010
|
+
Args:
|
|
3011
|
+
sql: The SQL query string to execute.
|
|
3012
|
+
"""
|
|
3013
|
+
# Add to history
|
|
3014
|
+
self._add_history(f"SQL Query:\n[$accent]{sql}[/]")
|
|
3015
|
+
|
|
3016
|
+
# Execute the SQL query
|
|
3017
|
+
try:
|
|
3018
|
+
self.df = self.df.sql(sql)
|
|
3019
|
+
except Exception as e:
|
|
3020
|
+
self.notify(f"Error executing SQL query [$error]{sql}[/]: {str(e)}", title="SQL Query", severity="error")
|
|
3021
|
+
return
|
|
3022
|
+
|
|
3023
|
+
if not len(self.df):
|
|
3024
|
+
self.notify(f"SQL query returned no results for [$warning]{sql}[/]", title="SQL Query", severity="warning")
|
|
3025
|
+
return
|
|
3026
|
+
|
|
3027
|
+
# Recreate the table display
|
|
3028
|
+
self._setup_table()
|
|
3029
|
+
|
|
3030
|
+
self.notify(
|
|
3031
|
+
f"SQL query executed successfully. Now showing [$accent]{len(self.df)}[/] rows and [$accent]{len(self.df.columns)}[/] columns.",
|
|
3032
|
+
title="SQL Query",
|
|
3033
|
+
)
|