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
|
@@ -10,7 +10,7 @@ from textual.app import App, ComposeResult
|
|
|
10
10
|
from textual.css.query import NoMatches
|
|
11
11
|
from textual.theme import BUILTIN_THEMES
|
|
12
12
|
from textual.widgets import TabbedContent, TabPane
|
|
13
|
-
from textual.widgets.tabbed_content import
|
|
13
|
+
from textual.widgets.tabbed_content import ContentTabs
|
|
14
14
|
|
|
15
15
|
from .common import get_next_item, load_file
|
|
16
16
|
from .data_frame_help_panel import DataFrameHelpPanel
|
|
@@ -26,7 +26,7 @@ class DataFrameViewer(App):
|
|
|
26
26
|
|
|
27
27
|
## 🎯 File & Tab Management
|
|
28
28
|
- **Ctrl+O** - 📁 Add a new tab
|
|
29
|
-
- **Ctrl+
|
|
29
|
+
- **Ctrl+A** - 💾 Save all tabs
|
|
30
30
|
- **Ctrl+W** - ❌ Close current tab
|
|
31
31
|
- **>** or **b** - ▶️ Next tab
|
|
32
32
|
- **<** - ◀️ Previous tab
|
|
@@ -34,7 +34,7 @@ class DataFrameViewer(App):
|
|
|
34
34
|
- **q** - 🚪 Quit application
|
|
35
35
|
|
|
36
36
|
## 🎨 View & Settings
|
|
37
|
-
- **
|
|
37
|
+
- **F1** - ❓ Toggle this help panel
|
|
38
38
|
- **k** - 🌙 Cycle through themes
|
|
39
39
|
|
|
40
40
|
## ⭐ Features
|
|
@@ -51,30 +51,25 @@ class DataFrameViewer(App):
|
|
|
51
51
|
|
|
52
52
|
BINDINGS = [
|
|
53
53
|
("q", "quit", "Quit"),
|
|
54
|
-
("
|
|
54
|
+
("f1", "toggle_help_panel", "Help"),
|
|
55
55
|
("B", "toggle_tab_bar", "Toggle Tab Bar"),
|
|
56
56
|
("ctrl+o", "add_tab", "Add Tab"),
|
|
57
|
-
("ctrl+
|
|
57
|
+
("ctrl+a", "save_all_tabs", "Save All Tabs"),
|
|
58
58
|
("ctrl+w", "close_tab", "Close Tab"),
|
|
59
59
|
("greater_than_sign,b", "next_tab(1)", "Next Tab"),
|
|
60
60
|
("less_than_sign", "next_tab(-1)", "Prev Tab"),
|
|
61
61
|
]
|
|
62
62
|
|
|
63
63
|
CSS = """
|
|
64
|
-
TabbedContent {
|
|
65
|
-
height: 100%; /* Or a specific value, e.g., 20; */
|
|
66
|
-
}
|
|
67
64
|
TabbedContent > ContentTabs {
|
|
68
65
|
dock: bottom;
|
|
69
66
|
}
|
|
70
67
|
TabbedContent > ContentSwitcher {
|
|
71
68
|
overflow: auto;
|
|
72
|
-
height: 1fr;
|
|
69
|
+
height: 1fr;
|
|
73
70
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
background: $primary;
|
|
77
|
-
color: $text;
|
|
71
|
+
ContentTab.-active {
|
|
72
|
+
background: $block-cursor-background; /* Same as underline */
|
|
78
73
|
}
|
|
79
74
|
"""
|
|
80
75
|
|
|
@@ -178,12 +173,6 @@ class DataFrameViewer(App):
|
|
|
178
173
|
if table.loaded_rows == 0:
|
|
179
174
|
table._setup_table()
|
|
180
175
|
|
|
181
|
-
# Apply background color to active tab
|
|
182
|
-
event.tab.add_class("active")
|
|
183
|
-
for tab in self.tabbed.query(ContentTab):
|
|
184
|
-
if tab != event.tab:
|
|
185
|
-
tab.remove_class("active")
|
|
186
|
-
|
|
187
176
|
def action_toggle_help_panel(self) -> None:
|
|
188
177
|
"""Toggle the help panel on or off.
|
|
189
178
|
|
|
@@ -305,15 +294,15 @@ class DataFrameViewer(App):
|
|
|
305
294
|
try:
|
|
306
295
|
n_tab = 0
|
|
307
296
|
for lf, filename, tabname in load_file(filename, prefix_sheet=True):
|
|
308
|
-
self._add_tab(lf
|
|
297
|
+
self._add_tab(lf, filename, tabname)
|
|
309
298
|
n_tab += 1
|
|
310
|
-
self.notify(f"Added [$accent]{n_tab}[/] tab(s) for [$success]{filename}[/]", title="Open")
|
|
299
|
+
# self.notify(f"Added [$accent]{n_tab}[/] tab(s) for [$success]{filename}[/]", title="Open")
|
|
311
300
|
except Exception as e:
|
|
312
|
-
self.notify(f"Error: {e}", title="Open", severity="error")
|
|
301
|
+
self.notify(f"Error loading [$error]{filename}[/]: {str(e)}", title="Open", severity="error")
|
|
313
302
|
else:
|
|
314
303
|
self.notify(f"File does not exist: [$warning]{filename}[/]", title="Open", severity="warning")
|
|
315
304
|
|
|
316
|
-
def _add_tab(self, df: pl.DataFrame, filename: str, tabname: str) -> None:
|
|
305
|
+
def _add_tab(self, df: pl.DataFrame | pl.LazyFrame, filename: str, tabname: str) -> None:
|
|
317
306
|
"""Add new tab for the given DataFrame.
|
|
318
307
|
|
|
319
308
|
Creates and adds a new tab with the provided DataFrame and configuration.
|
|
@@ -321,15 +310,18 @@ class DataFrameViewer(App):
|
|
|
321
310
|
if this is no longer the only tab.
|
|
322
311
|
|
|
323
312
|
Args:
|
|
324
|
-
|
|
313
|
+
lf: The Polars DataFrame to display in the new tab.
|
|
325
314
|
filename: The source filename for this data (used in table metadata).
|
|
326
315
|
tabname: The display name for the tab.
|
|
327
316
|
|
|
328
317
|
Returns:
|
|
329
318
|
None
|
|
330
319
|
"""
|
|
331
|
-
|
|
332
|
-
|
|
320
|
+
# Ensure unique tab names
|
|
321
|
+
counter = 1
|
|
322
|
+
while any(tab.name == tabname for tab in self.tabs):
|
|
323
|
+
tabname = f"{tabname}_{counter}"
|
|
324
|
+
counter += 1
|
|
333
325
|
|
|
334
326
|
# Find an available tab index
|
|
335
327
|
tab_idx = f"tab_{len(self.tabs) + 1}"
|
|
@@ -369,6 +361,6 @@ class DataFrameViewer(App):
|
|
|
369
361
|
if active_pane := self.tabbed.active_pane:
|
|
370
362
|
self.tabbed.remove_pane(active_pane.id)
|
|
371
363
|
self.tabs.pop(active_pane)
|
|
372
|
-
self.notify(f"Closed tab [$success]{active_pane.name}[/]", title="Close")
|
|
364
|
+
# self.notify(f"Closed tab [$success]{active_pane.name}[/]", title="Close")
|
|
373
365
|
except NoMatches:
|
|
374
366
|
pass
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Modal screens for Polars sql manipulation"""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .data_frame_table import DataFrameTable
|
|
7
|
+
|
|
8
|
+
import polars as pl
|
|
9
|
+
from textual.app import ComposeResult
|
|
10
|
+
from textual.containers import Container, Horizontal
|
|
11
|
+
from textual.screen import ModalScreen
|
|
12
|
+
from textual.widgets import Button, Input, Label, SelectionList, TextArea
|
|
13
|
+
from textual.widgets.selection_list import Selection
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SqlScreen(ModalScreen):
|
|
17
|
+
"""Base class for modal screens handling SQL query."""
|
|
18
|
+
|
|
19
|
+
DEFAULT_CSS = """
|
|
20
|
+
SqlScreen {
|
|
21
|
+
align: center middle;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
SqlScreen > Container {
|
|
25
|
+
width: auto;
|
|
26
|
+
height: auto;
|
|
27
|
+
border: heavy $accent;
|
|
28
|
+
border-title-color: $accent;
|
|
29
|
+
border-title-background: $panel;
|
|
30
|
+
border-title-style: bold;
|
|
31
|
+
background: $background;
|
|
32
|
+
padding: 1 2;
|
|
33
|
+
overflow: auto;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#button-container {
|
|
37
|
+
width: auto;
|
|
38
|
+
margin: 1 0 0 0;
|
|
39
|
+
height: 3;
|
|
40
|
+
align: center middle;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Button {
|
|
44
|
+
margin: 0 2;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, dftable: "DataFrameTable", on_yes_callback=None) -> None:
|
|
50
|
+
"""Initialize the SQL screen."""
|
|
51
|
+
super().__init__()
|
|
52
|
+
self.dftable = dftable # DataFrameTable
|
|
53
|
+
self.df: pl.DataFrame = dftable.df # Polars DataFrame
|
|
54
|
+
self.on_yes_callback = on_yes_callback
|
|
55
|
+
|
|
56
|
+
def compose(self) -> ComposeResult:
|
|
57
|
+
"""Compose the SQL screen widget structure."""
|
|
58
|
+
# Shared by subclasses
|
|
59
|
+
with Horizontal(id="button-container"):
|
|
60
|
+
yield Button("Apply", id="yes", variant="success")
|
|
61
|
+
yield Button("Cancel", id="no", variant="error")
|
|
62
|
+
|
|
63
|
+
def on_key(self, event) -> None:
|
|
64
|
+
"""Handle key press events in the SQL screen"""
|
|
65
|
+
if event.key in ("q", "escape"):
|
|
66
|
+
self.app.pop_screen()
|
|
67
|
+
event.stop()
|
|
68
|
+
elif event.key == "enter":
|
|
69
|
+
self._handle_yes()
|
|
70
|
+
event.stop()
|
|
71
|
+
elif event.key == "escape":
|
|
72
|
+
self.dismiss(None)
|
|
73
|
+
event.stop()
|
|
74
|
+
|
|
75
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
76
|
+
"""Handle button press events in the SQL screen."""
|
|
77
|
+
if event.button.id == "yes":
|
|
78
|
+
self._handle_yes()
|
|
79
|
+
elif event.button.id == "no":
|
|
80
|
+
self.dismiss(None)
|
|
81
|
+
|
|
82
|
+
def _handle_yes(self) -> None:
|
|
83
|
+
"""Handle Yes button/Enter key press."""
|
|
84
|
+
if self.on_yes_callback:
|
|
85
|
+
result = self.on_yes_callback()
|
|
86
|
+
self.dismiss(result)
|
|
87
|
+
else:
|
|
88
|
+
self.dismiss(True)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class SimpleSqlScreen(SqlScreen):
|
|
92
|
+
"""Simple SQL query screen."""
|
|
93
|
+
|
|
94
|
+
DEFAULT_CSS = SqlScreen.DEFAULT_CSS.replace("SqlScreen", "SimpleSqlScreen")
|
|
95
|
+
|
|
96
|
+
CSS = """
|
|
97
|
+
SimpleSqlScreen SelectionList {
|
|
98
|
+
width: auto;
|
|
99
|
+
min-width: 40;
|
|
100
|
+
margin: 1 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
SimpleSqlScreen SelectionList:blur {
|
|
104
|
+
border: solid $secondary;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
SimpleSqlScreen Label {
|
|
108
|
+
width: auto;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
SimpleSqlScreen Input {
|
|
112
|
+
width: auto;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
SimpleSqlScreen Input:blur {
|
|
116
|
+
border: solid $secondary;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#button-container {
|
|
120
|
+
min-width: 40;
|
|
121
|
+
}
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(self, dftable: "DataFrameTable") -> None:
|
|
125
|
+
"""Initialize the simple SQL screen.
|
|
126
|
+
|
|
127
|
+
Sets up the modal screen with reference to the main DataFrameTable widget
|
|
128
|
+
and stores the DataFrame for display.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
dftable: Reference to the parent DataFrameTable widget.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
None
|
|
135
|
+
"""
|
|
136
|
+
super().__init__(dftable, on_yes_callback=self._handle_simple)
|
|
137
|
+
|
|
138
|
+
def compose(self) -> ComposeResult:
|
|
139
|
+
"""Compose the simple SQL screen widget structure."""
|
|
140
|
+
with Container(id="sql-container") as container:
|
|
141
|
+
container.border_title = "SQL Query"
|
|
142
|
+
yield Label("Select columns (default to all):", id="select-label")
|
|
143
|
+
yield SelectionList(*[Selection(col, col) for col in self.df.columns], id="column-selection")
|
|
144
|
+
yield Label("Where condition (optional)", id="where-label")
|
|
145
|
+
yield Input(placeholder="e.g., age > 30 and height < 180", id="where-input")
|
|
146
|
+
yield from super().compose()
|
|
147
|
+
|
|
148
|
+
def _handle_simple(self) -> None:
|
|
149
|
+
"""Handle Yes button/Enter key press."""
|
|
150
|
+
selections = self.query_one(SelectionList).selected
|
|
151
|
+
columns = ", ".join(f"`{s}`" for s in selections) if selections else "*"
|
|
152
|
+
where = self.query_one(Input).value.strip()
|
|
153
|
+
|
|
154
|
+
return columns, where
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class AdvancedSqlScreen(SqlScreen):
|
|
158
|
+
"""Advanced SQL query screen."""
|
|
159
|
+
|
|
160
|
+
DEFAULT_CSS = SqlScreen.DEFAULT_CSS.replace("SqlScreen", "AdvancedSqlScreen")
|
|
161
|
+
|
|
162
|
+
CSS = """
|
|
163
|
+
AdvancedSqlScreen TextArea {
|
|
164
|
+
width: auto;
|
|
165
|
+
min-width: 60;
|
|
166
|
+
height: auto;
|
|
167
|
+
min-height: 10;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#button-container {
|
|
171
|
+
min-width: 60;
|
|
172
|
+
}
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
def __init__(self, dftable: "DataFrameTable") -> None:
|
|
176
|
+
"""Initialize the simple SQL screen.
|
|
177
|
+
|
|
178
|
+
Sets up the modal screen with reference to the main DataFrameTable widget
|
|
179
|
+
and stores the DataFrame for display.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
dftable: Reference to the parent DataFrameTable widget.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
None
|
|
186
|
+
"""
|
|
187
|
+
super().__init__(dftable, on_yes_callback=self._handle_advanced)
|
|
188
|
+
|
|
189
|
+
def compose(self) -> ComposeResult:
|
|
190
|
+
"""Compose the advanced SQL screen widget structure."""
|
|
191
|
+
with Container(id="sql-container") as container:
|
|
192
|
+
container.border_title = "Advanced SQL Query"
|
|
193
|
+
yield TextArea.code_editor(
|
|
194
|
+
placeholder="Enter SQL query (use `self` as the table name), e.g., \n\nSELECT * \nFROM self \nWHERE age > 30",
|
|
195
|
+
id="sql-textarea",
|
|
196
|
+
language="sql",
|
|
197
|
+
)
|
|
198
|
+
yield from super().compose()
|
|
199
|
+
|
|
200
|
+
def _handle_advanced(self) -> None:
|
|
201
|
+
"""Handle Yes button/Enter key press."""
|
|
202
|
+
return self.query_one(TextArea).text.strip()
|
|
@@ -37,7 +37,7 @@ class TableScreen(ModalScreen):
|
|
|
37
37
|
}
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
|
-
def __init__(self, dftable: DataFrameTable) -> None:
|
|
40
|
+
def __init__(self, dftable: "DataFrameTable") -> None:
|
|
41
41
|
"""Initialize the table screen.
|
|
42
42
|
|
|
43
43
|
Sets up the base modal screen with reference to the main DataFrameTable widget
|
|
@@ -50,8 +50,8 @@ class TableScreen(ModalScreen):
|
|
|
50
50
|
None
|
|
51
51
|
"""
|
|
52
52
|
super().__init__()
|
|
53
|
-
self.df: pl.DataFrame = dftable.df # Polars DataFrame
|
|
54
53
|
self.dftable = dftable # DataFrameTable
|
|
54
|
+
self.df: pl.DataFrame = dftable.df # Polars DataFrame
|
|
55
55
|
self.thousand_separator = False # Whether to use thousand separators in numbers
|
|
56
56
|
|
|
57
57
|
def compose(self) -> ComposeResult:
|
|
@@ -225,7 +225,7 @@ class StatisticsScreen(TableScreen):
|
|
|
225
225
|
|
|
226
226
|
CSS = TableScreen.DEFAULT_CSS.replace("TableScreen", "StatisticsScreen")
|
|
227
227
|
|
|
228
|
-
def __init__(self, dftable: DataFrameTable, col_idx: int | None = None):
|
|
228
|
+
def __init__(self, dftable: "DataFrameTable", col_idx: int | None = None):
|
|
229
229
|
super().__init__(dftable)
|
|
230
230
|
self.col_idx = col_idx # None for dataframe statistics, otherwise column index
|
|
231
231
|
|
|
@@ -344,15 +344,16 @@ class FrequencyScreen(TableScreen):
|
|
|
344
344
|
|
|
345
345
|
CSS = TableScreen.DEFAULT_CSS.replace("TableScreen", "FrequencyScreen")
|
|
346
346
|
|
|
347
|
-
def __init__(self, col_idx: int, dftable: DataFrameTable):
|
|
347
|
+
def __init__(self, col_idx: int, dftable: "DataFrameTable") -> None:
|
|
348
348
|
super().__init__(dftable)
|
|
349
349
|
self.col_idx = col_idx
|
|
350
350
|
self.sorted_columns = {
|
|
351
351
|
1: True, # Count
|
|
352
352
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
)
|
|
353
|
+
|
|
354
|
+
df = dftable.df.filter(dftable.visible_rows) if False in dftable.visible_rows else dftable.df
|
|
355
|
+
self.total_count = len(df)
|
|
356
|
+
self.df: pl.DataFrame = df[df.columns[self.col_idx]].value_counts(sort=True).sort("count", descending=True)
|
|
356
357
|
|
|
357
358
|
def on_mount(self) -> None:
|
|
358
359
|
"""Create the frequency table."""
|
|
@@ -385,9 +386,6 @@ class FrequencyScreen(TableScreen):
|
|
|
385
386
|
dtype = self.dftable.df.dtypes[self.col_idx]
|
|
386
387
|
dc = DtypeConfig(dtype)
|
|
387
388
|
|
|
388
|
-
# Calculate frequencies using Polars
|
|
389
|
-
total_count = len(self.dftable.df)
|
|
390
|
-
|
|
391
389
|
# Add column headers with sort indicators
|
|
392
390
|
columns = [
|
|
393
391
|
(column, "Value", 0),
|
|
@@ -415,7 +413,7 @@ class FrequencyScreen(TableScreen):
|
|
|
415
413
|
# Add rows to the frequency table
|
|
416
414
|
for row_idx, row in enumerate(self.df.rows()):
|
|
417
415
|
column, count = row
|
|
418
|
-
percentage = (count / total_count) * 100
|
|
416
|
+
percentage = (count / self.total_count) * 100
|
|
419
417
|
|
|
420
418
|
if column is None:
|
|
421
419
|
value = NULL_DISPLAY
|
|
@@ -446,7 +444,7 @@ class FrequencyScreen(TableScreen):
|
|
|
446
444
|
# Add a total row
|
|
447
445
|
self.table.add_row(
|
|
448
446
|
Text("Total", style="bold", justify=dc.justify),
|
|
449
|
-
Text(f"{total_count:,}", style="bold", justify="right"),
|
|
447
|
+
Text(f"{self.total_count:,}", style="bold", justify="right"),
|
|
450
448
|
Text("100.00", style="bold", justify="right"),
|
|
451
449
|
Bar(
|
|
452
450
|
highlight_range=(0.0, 10),
|
|
@@ -460,12 +458,10 @@ class FrequencyScreen(TableScreen):
|
|
|
460
458
|
row_idx, col_idx = self.table.cursor_coordinate
|
|
461
459
|
col_sort = col_idx if col_idx == 0 else 1
|
|
462
460
|
|
|
463
|
-
|
|
464
|
-
if sort_dir is not None:
|
|
461
|
+
if self.sorted_columns.get(col_sort) == descending:
|
|
465
462
|
# If already sorted in the same direction, do nothing
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
return
|
|
463
|
+
# self.notify("Already sorted in that order", title="Sort", severity="warning")
|
|
464
|
+
return
|
|
469
465
|
|
|
470
466
|
self.sorted_columns.clear()
|
|
471
467
|
self.sorted_columns[col_sort] = descending
|
|
@@ -33,9 +33,11 @@ class YesNoScreen(ModalScreen):
|
|
|
33
33
|
min-width: 40;
|
|
34
34
|
max-width: 60;
|
|
35
35
|
height: auto;
|
|
36
|
-
border: heavy $
|
|
37
|
-
border-title-color: $
|
|
38
|
-
background: $
|
|
36
|
+
border: heavy $accent;
|
|
37
|
+
border-title-color: $accent;
|
|
38
|
+
border-title-background: $panel;
|
|
39
|
+
border-title-style: bold;
|
|
40
|
+
background: $background;
|
|
39
41
|
padding: 1 2;
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -241,6 +243,7 @@ class YesNoScreen(ModalScreen):
|
|
|
241
243
|
yield self.no
|
|
242
244
|
|
|
243
245
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
246
|
+
"""Handle button press events in the Yes/No screen."""
|
|
244
247
|
if event.button.id == "yes":
|
|
245
248
|
self._handle_yes()
|
|
246
249
|
elif event.button.id == "maybe":
|
|
@@ -249,6 +252,7 @@ class YesNoScreen(ModalScreen):
|
|
|
249
252
|
self.dismiss(None)
|
|
250
253
|
|
|
251
254
|
def on_key(self, event) -> None:
|
|
255
|
+
"""Handle key press events in the table screen."""
|
|
252
256
|
if event.key == "enter":
|
|
253
257
|
self._handle_yes()
|
|
254
258
|
event.stop()
|
|
@@ -609,7 +613,7 @@ class AddColumnScreen(YesNoScreen):
|
|
|
609
613
|
return self.cidx, col_name, pl.lit(None)
|
|
610
614
|
elif tentative_expr(term):
|
|
611
615
|
try:
|
|
612
|
-
expr = validate_expr(term, self.df, self.cidx)
|
|
616
|
+
expr = validate_expr(term, self.df.columns, self.cidx)
|
|
613
617
|
return self.cidx, col_name, expr
|
|
614
618
|
except ValueError as e:
|
|
615
619
|
self.notify(f"Invalid expression [$error]{term}[/]: {str(e)}", title="Add Column", severity="error")
|
|
@@ -634,7 +638,7 @@ class FindReplaceScreen(YesNoScreen):
|
|
|
634
638
|
|
|
635
639
|
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "ReplaceScreen")
|
|
636
640
|
|
|
637
|
-
def __init__(self, dftable: DataFrameTable):
|
|
641
|
+
def __init__(self, dftable: "DataFrameTable"):
|
|
638
642
|
term_find = str(dftable.cursor_value)
|
|
639
643
|
super().__init__(
|
|
640
644
|
title="Find and Replace",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataframe-textual
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.9
|
|
4
4
|
Summary: Interactive terminal viewer/editor for tabular data
|
|
5
5
|
Project-URL: Homepage, https://github.com/need47/dataframe-textual
|
|
6
6
|
Project-URL: Repository, https://github.com/need47/dataframe-textual.git
|
|
@@ -29,7 +29,7 @@ Classifier: Topic :: Utilities
|
|
|
29
29
|
Classifier: Typing :: Typed
|
|
30
30
|
Requires-Python: >=3.11
|
|
31
31
|
Requires-Dist: polars>=1.34.0
|
|
32
|
-
Requires-Dist: textual>=6.5.0
|
|
32
|
+
Requires-Dist: textual[syntax]>=6.5.0
|
|
33
33
|
Provides-Extra: dev
|
|
34
34
|
Requires-Dist: textual-dev>=1.8.0; extra == 'dev'
|
|
35
35
|
Provides-Extra: excel
|
|
@@ -161,7 +161,7 @@ When multiple files are opened:
|
|
|
161
161
|
|-----|--------|
|
|
162
162
|
| `Ctrl+O` | Open file in a new tab |
|
|
163
163
|
| `Ctrl+W` | Close current tab |
|
|
164
|
-
| `Ctrl+
|
|
164
|
+
| `Ctrl+A` | Save all open tabs to Excel file |
|
|
165
165
|
| `>` or `b` | Move to next tab |
|
|
166
166
|
| `<` | Move to previous tab |
|
|
167
167
|
| `B` | Toggle tab bar visibility |
|
|
@@ -171,7 +171,7 @@ When multiple files are opened:
|
|
|
171
171
|
|
|
172
172
|
| Key | Action |
|
|
173
173
|
|-----|--------|
|
|
174
|
-
| `
|
|
174
|
+
| `F1` | Toggle help panel |
|
|
175
175
|
| `k` | Cycle through themes |
|
|
176
176
|
|
|
177
177
|
---
|
|
@@ -213,7 +213,7 @@ When multiple files are opened:
|
|
|
213
213
|
| `A` | Add column with name and value/expression |
|
|
214
214
|
| `-` (minus) | Delete current column |
|
|
215
215
|
| `_` (underscore) | Delete current column and all columns after |
|
|
216
|
-
| `Ctrl
|
|
216
|
+
| `Ctrl+_` | Delete current column and all columns before |
|
|
217
217
|
| `x` | Delete current row |
|
|
218
218
|
| `X` | Delete current row and all rows below |
|
|
219
219
|
| `Ctrl+X` | Delete current row and all rows above |
|
|
@@ -241,6 +241,13 @@ When multiple files are opened:
|
|
|
241
241
|
| `v` | View only rows by selected rows and/or matches or cursor value |
|
|
242
242
|
| `V` | View only rows by expression |
|
|
243
243
|
|
|
244
|
+
#### SQL Interface
|
|
245
|
+
|
|
246
|
+
| Key | Action |
|
|
247
|
+
|-----|--------|
|
|
248
|
+
| `l` | Simple SQL interface (select columns & WHERE clause) |
|
|
249
|
+
| `L` | Advanced SQL interface (full SQL queries) |
|
|
250
|
+
|
|
244
251
|
#### Find & Replace
|
|
245
252
|
|
|
246
253
|
| Key | Action |
|
|
@@ -703,7 +710,40 @@ Press `@` to make URLs in the current column clickable:
|
|
|
703
710
|
- **Scans** all cells in the current column for URLs starting with `http://` or `https://`
|
|
704
711
|
- **Applies** link styling to make them clickable and dataframe remains unchanged
|
|
705
712
|
|
|
706
|
-
### 19.
|
|
713
|
+
### 19. SQL Interface
|
|
714
|
+
|
|
715
|
+
The SQL interface provides two modes for querying your dataframe:
|
|
716
|
+
|
|
717
|
+
#### Simple SQL Interface (`l`)
|
|
718
|
+
Select specific columns and apply WHERE conditions without writing full SQL:
|
|
719
|
+
- Choose which columns to include in results
|
|
720
|
+
- Specify WHERE clause for filtering
|
|
721
|
+
- Ideal for quick filtering and column selection
|
|
722
|
+
|
|
723
|
+
#### Advanced SQL Interface (`L`)
|
|
724
|
+
Execute complete SQL queries for advanced data manipulation:
|
|
725
|
+
- Write full SQL queries with standard [SQL syntax](https://docs.pola.rs/api/python/stable/reference/sql/index.html)
|
|
726
|
+
- Support for JOINs, GROUP BY, aggregations, and more
|
|
727
|
+
- Access to all SQL capabilities for complex transformations
|
|
728
|
+
- Always use `self` as the table name
|
|
729
|
+
|
|
730
|
+
**Examples:**
|
|
731
|
+
```sql
|
|
732
|
+
-- Filter and select specific rows and/or columns
|
|
733
|
+
SELECT name, age FROM self WHERE age > 30
|
|
734
|
+
|
|
735
|
+
-- Aggregate with GROUP BY
|
|
736
|
+
SELECT department, COUNT(*) as count, AVG(salary) as avg_salary
|
|
737
|
+
FROM self
|
|
738
|
+
GROUP BY department
|
|
739
|
+
|
|
740
|
+
-- Complex filtering with multiple conditions
|
|
741
|
+
SELECT *
|
|
742
|
+
FROM self
|
|
743
|
+
WHERE (age > 25 AND salary > 50000) OR department = 'Management'
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
### 20. Clipboard Operations
|
|
707
747
|
|
|
708
748
|
Copies value to system clipboard with `pbcopy` on macOS and `xclip` on Linux
|
|
709
749
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
dataframe_textual/__init__.py,sha256=IFPb8RMUgghw0eRomehkkC684Iny_gs1VkiZMQ5ZpFk,813
|
|
2
|
+
dataframe_textual/__main__.py,sha256=hgjKLT3ggGxVVeRBunkArTy7PTqOHVJUf3fsH4P5dfU,2146
|
|
3
|
+
dataframe_textual/common.py,sha256=YeXikgy7Mvgfs4vvoloG_8i2rBBnAsHDPnjaYQfIHUM,16748
|
|
4
|
+
dataframe_textual/data_frame_help_panel.py,sha256=iEKaur-aH1N_oqHu-vMwEEjfkjQiThK24UO5izsOiW0,3416
|
|
5
|
+
dataframe_textual/data_frame_table.py,sha256=FIxhKvVEzm4wrejFu7iX_zoZFDAxO0xs0o80aeF0Km0,114086
|
|
6
|
+
dataframe_textual/data_frame_viewer.py,sha256=CFZv4UdD2JU_uTmXccVznb_K7NmxmFMsDkYoZBZlkc0,12716
|
|
7
|
+
dataframe_textual/sql_screen.py,sha256=F-4C4AwjdyiXzLl6fheGrkVMfk-Yvz5EXaXPFfZq5xs,6165
|
|
8
|
+
dataframe_textual/table_screen.py,sha256=o5gLWiF01oa2beTjCCqTG87qMs7XDZgkvqjfsWRU9K4,17755
|
|
9
|
+
dataframe_textual/yes_no_screen.py,sha256=gI4yj1T5rek10A9Va11KsuO_1XwVgvVPOUDrcknPrQM,23127
|
|
10
|
+
dataframe_textual-1.3.9.dist-info/METADATA,sha256=Q1a1gyNiWwMftSIPkw3XUWoVigM0KSfIpdIra0j7Wec,27924
|
|
11
|
+
dataframe_textual-1.3.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
12
|
+
dataframe_textual-1.3.9.dist-info/entry_points.txt,sha256=R_GoooOxcq6ab4RaHiVoZ4zrZJ-phMcGmlL2rwqncW8,107
|
|
13
|
+
dataframe_textual-1.3.9.dist-info/licenses/LICENSE,sha256=AVTg0gk1X-LHI-nnHlAMDQetrwuDZK4eypgSMDO46Yc,1069
|
|
14
|
+
dataframe_textual-1.3.9.dist-info/RECORD,,
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
dataframe_textual/__init__.py,sha256=IFPb8RMUgghw0eRomehkkC684Iny_gs1VkiZMQ5ZpFk,813
|
|
2
|
-
dataframe_textual/__main__.py,sha256=hgjKLT3ggGxVVeRBunkArTy7PTqOHVJUf3fsH4P5dfU,2146
|
|
3
|
-
dataframe_textual/common.py,sha256=q4bXS7oiJAsdcMEfHkPm5-e8SlfcwCBNhFN9TinQqV0,16171
|
|
4
|
-
dataframe_textual/data_frame_help_panel.py,sha256=XgKGEPJr2hnDWpZ5mavLRcBSPa9cvrXdzVUGFQavXm4,3353
|
|
5
|
-
dataframe_textual/data_frame_table.py,sha256=UbUlPwrcLbN_yNz9ECMgKHypTgVV5qDLvSQUJsDby8Q,109452
|
|
6
|
-
dataframe_textual/data_frame_viewer.py,sha256=4mV3k7MNTf9TKBmGJ8fDx7itA1vo4qSmaWpvZozwfjs,12987
|
|
7
|
-
dataframe_textual/table_screen.py,sha256=KCmvKAdHexIFQKGob6WMPrcAITHO6CMVpRJzzs0pbrE,17793
|
|
8
|
-
dataframe_textual/yes_no_screen.py,sha256=vyUKMBbbwgt5At1U430eLg3WbJvqUNoz2GpvdnMd7q0,22921
|
|
9
|
-
dataframe_textual-1.2.0.dist-info/METADATA,sha256=zLQtFr7h6-7fZ5dJoNYjGX5cghyKpGDKX0c7-wSbUy0,26688
|
|
10
|
-
dataframe_textual-1.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
-
dataframe_textual-1.2.0.dist-info/entry_points.txt,sha256=Z0XKp1ACvmbJymekkxt-C81I0OoInksr5Ib0w2OT_a4,55
|
|
12
|
-
dataframe_textual-1.2.0.dist-info/licenses/LICENSE,sha256=AVTg0gk1X-LHI-nnHlAMDQetrwuDZK4eypgSMDO46Yc,1069
|
|
13
|
-
dataframe_textual-1.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|