dataframe-textual 1.5.0__py3-none-any.whl → 1.9.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.
- dataframe_textual/__main__.py +1 -1
- dataframe_textual/common.py +15 -7
- dataframe_textual/data_frame_table.py +979 -879
- dataframe_textual/data_frame_viewer.py +317 -101
- dataframe_textual/sql_screen.py +50 -11
- dataframe_textual/table_screen.py +1 -1
- dataframe_textual/yes_no_screen.py +66 -5
- {dataframe_textual-1.5.0.dist-info → dataframe_textual-1.9.0.dist-info}/METADATA +106 -245
- dataframe_textual-1.9.0.dist-info/RECORD +14 -0
- dataframe_textual-1.5.0.dist-info/RECORD +0 -14
- {dataframe_textual-1.5.0.dist-info → dataframe_textual-1.9.0.dist-info}/WHEEL +0 -0
- {dataframe_textual-1.5.0.dist-info → dataframe_textual-1.9.0.dist-info}/entry_points.txt +0 -0
- {dataframe_textual-1.5.0.dist-info → dataframe_textual-1.9.0.dist-info}/licenses/LICENSE +0 -0
dataframe_textual/sql_screen.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Modal screens for Polars sql manipulation"""
|
|
2
2
|
|
|
3
|
+
from functools import partial
|
|
3
4
|
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
if TYPE_CHECKING:
|
|
@@ -46,18 +47,20 @@ class SqlScreen(ModalScreen):
|
|
|
46
47
|
|
|
47
48
|
"""
|
|
48
49
|
|
|
49
|
-
def __init__(self, dftable: "DataFrameTable", on_yes_callback=None) -> None:
|
|
50
|
+
def __init__(self, dftable: "DataFrameTable", on_yes_callback=None, on_maybe_callback=None) -> None:
|
|
50
51
|
"""Initialize the SQL screen."""
|
|
51
52
|
super().__init__()
|
|
52
53
|
self.dftable = dftable # DataFrameTable
|
|
53
54
|
self.df: pl.DataFrame = dftable.df # Polars DataFrame
|
|
54
55
|
self.on_yes_callback = on_yes_callback
|
|
56
|
+
self.on_maybe_callback = on_maybe_callback
|
|
55
57
|
|
|
56
58
|
def compose(self) -> ComposeResult:
|
|
57
59
|
"""Compose the SQL screen widget structure."""
|
|
58
60
|
# Shared by subclasses
|
|
59
61
|
with Horizontal(id="button-container"):
|
|
60
|
-
yield Button("
|
|
62
|
+
yield Button("View", id="yes", variant="success")
|
|
63
|
+
yield Button("Filter", id="maybe", variant="warning")
|
|
61
64
|
yield Button("Cancel", id="no", variant="error")
|
|
62
65
|
|
|
63
66
|
def on_key(self, event) -> None:
|
|
@@ -66,7 +69,16 @@ class SqlScreen(ModalScreen):
|
|
|
66
69
|
self.app.pop_screen()
|
|
67
70
|
event.stop()
|
|
68
71
|
elif event.key == "enter":
|
|
69
|
-
self.
|
|
72
|
+
for button in self.query(Button):
|
|
73
|
+
if button.has_focus:
|
|
74
|
+
if button.id == "yes":
|
|
75
|
+
self._handle_yes()
|
|
76
|
+
elif button.id == "maybe":
|
|
77
|
+
self._handle_maybe()
|
|
78
|
+
break
|
|
79
|
+
else:
|
|
80
|
+
self._handle_yes()
|
|
81
|
+
|
|
70
82
|
event.stop()
|
|
71
83
|
elif event.key == "escape":
|
|
72
84
|
self.dismiss(None)
|
|
@@ -76,6 +88,8 @@ class SqlScreen(ModalScreen):
|
|
|
76
88
|
"""Handle button press events in the SQL screen."""
|
|
77
89
|
if event.button.id == "yes":
|
|
78
90
|
self._handle_yes()
|
|
91
|
+
elif event.button.id == "maybe":
|
|
92
|
+
self._handle_maybe()
|
|
79
93
|
elif event.button.id == "no":
|
|
80
94
|
self.dismiss(None)
|
|
81
95
|
|
|
@@ -87,6 +101,14 @@ class SqlScreen(ModalScreen):
|
|
|
87
101
|
else:
|
|
88
102
|
self.dismiss(True)
|
|
89
103
|
|
|
104
|
+
def _handle_maybe(self) -> None:
|
|
105
|
+
"""Handle Maybe button press."""
|
|
106
|
+
if self.on_maybe_callback:
|
|
107
|
+
result = self.on_maybe_callback()
|
|
108
|
+
self.dismiss(result)
|
|
109
|
+
else:
|
|
110
|
+
self.dismiss(True)
|
|
111
|
+
|
|
90
112
|
|
|
91
113
|
class SimpleSqlScreen(SqlScreen):
|
|
92
114
|
"""Simple SQL query screen."""
|
|
@@ -133,25 +155,38 @@ class SimpleSqlScreen(SqlScreen):
|
|
|
133
155
|
Returns:
|
|
134
156
|
None
|
|
135
157
|
"""
|
|
136
|
-
super().__init__(
|
|
158
|
+
super().__init__(
|
|
159
|
+
dftable,
|
|
160
|
+
on_yes_callback=self._handle_simple,
|
|
161
|
+
on_maybe_callback=partial(
|
|
162
|
+
self._handle_simple,
|
|
163
|
+
view=False,
|
|
164
|
+
),
|
|
165
|
+
)
|
|
137
166
|
|
|
138
167
|
def compose(self) -> ComposeResult:
|
|
139
168
|
"""Compose the simple SQL screen widget structure."""
|
|
140
169
|
with Container(id="sql-container") as container:
|
|
141
170
|
container.border_title = "SQL Query"
|
|
142
171
|
yield Label("Select columns (default to all):", id="select-label")
|
|
143
|
-
yield SelectionList(
|
|
172
|
+
yield SelectionList(
|
|
173
|
+
*[Selection(col, col) for col in self.df.columns if col not in self.dftable.hidden_columns],
|
|
174
|
+
id="column-selection",
|
|
175
|
+
)
|
|
144
176
|
yield Label("Where condition (optional)", id="where-label")
|
|
145
177
|
yield Input(placeholder="e.g., age > 30 and height < 180", id="where-input")
|
|
146
178
|
yield from super().compose()
|
|
147
179
|
|
|
148
|
-
def _handle_simple(self) -> None:
|
|
180
|
+
def _handle_simple(self, view: bool = True) -> None:
|
|
149
181
|
"""Handle Yes button/Enter key press."""
|
|
150
182
|
selections = self.query_one(SelectionList).selected
|
|
151
|
-
|
|
183
|
+
if not selections:
|
|
184
|
+
selections = [col for col in self.df.columns if col not in self.dftable.hidden_columns]
|
|
185
|
+
|
|
186
|
+
columns = ", ".join(f"`{s}`" for s in selections)
|
|
152
187
|
where = self.query_one(Input).value.strip()
|
|
153
188
|
|
|
154
|
-
return columns, where
|
|
189
|
+
return columns, where, view
|
|
155
190
|
|
|
156
191
|
|
|
157
192
|
class AdvancedSqlScreen(SqlScreen):
|
|
@@ -184,7 +219,11 @@ class AdvancedSqlScreen(SqlScreen):
|
|
|
184
219
|
Returns:
|
|
185
220
|
None
|
|
186
221
|
"""
|
|
187
|
-
super().__init__(
|
|
222
|
+
super().__init__(
|
|
223
|
+
dftable,
|
|
224
|
+
on_yes_callback=self._handle_advanced,
|
|
225
|
+
on_maybe_callback=partial(self._handle_advanced, view=False),
|
|
226
|
+
)
|
|
188
227
|
|
|
189
228
|
def compose(self) -> ComposeResult:
|
|
190
229
|
"""Compose the advanced SQL screen widget structure."""
|
|
@@ -197,6 +236,6 @@ class AdvancedSqlScreen(SqlScreen):
|
|
|
197
236
|
)
|
|
198
237
|
yield from super().compose()
|
|
199
238
|
|
|
200
|
-
def _handle_advanced(self) -> None:
|
|
239
|
+
def _handle_advanced(self, view: bool = True) -> None:
|
|
201
240
|
"""Handle Yes button/Enter key press."""
|
|
202
|
-
return self.query_one(TextArea).text.strip()
|
|
241
|
+
return self.query_one(TextArea).text.strip(), view
|
|
@@ -144,7 +144,7 @@ class TableScreen(ModalScreen):
|
|
|
144
144
|
message = f"Highlighted [$accent]{col_name}[/] == [$success]{value_display}[/]"
|
|
145
145
|
|
|
146
146
|
# Recreate the table display with updated data in the main app
|
|
147
|
-
self.dftable.
|
|
147
|
+
self.dftable.setup_table()
|
|
148
148
|
|
|
149
149
|
# Dismiss the frequency screen
|
|
150
150
|
self.app.pop_screen()
|
|
@@ -5,11 +5,13 @@ from typing import TYPE_CHECKING
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from .data_frame_table import DataFrameTable
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
import polars as pl
|
|
9
10
|
from textual.app import ComposeResult
|
|
10
11
|
from textual.containers import Container, Horizontal
|
|
11
12
|
from textual.screen import ModalScreen
|
|
12
|
-
from textual.widgets import Button, Checkbox, Input, Label, Static
|
|
13
|
+
from textual.widgets import Button, Checkbox, Input, Label, Static, TabPane
|
|
14
|
+
from textual.widgets.tabbed_content import ContentTab
|
|
13
15
|
|
|
14
16
|
from .common import NULL, DtypeConfig, tentative_expr, validate_expr
|
|
15
17
|
|
|
@@ -254,7 +256,18 @@ class YesNoScreen(ModalScreen):
|
|
|
254
256
|
def on_key(self, event) -> None:
|
|
255
257
|
"""Handle key press events in the table screen."""
|
|
256
258
|
if event.key == "enter":
|
|
257
|
-
self.
|
|
259
|
+
for button in self.query(Button):
|
|
260
|
+
if button.has_focus:
|
|
261
|
+
if button.id == "yes":
|
|
262
|
+
self._handle_yes()
|
|
263
|
+
elif button.id == "maybe":
|
|
264
|
+
self._handle_maybe()
|
|
265
|
+
elif button.id == "no":
|
|
266
|
+
self.dismiss(None)
|
|
267
|
+
break
|
|
268
|
+
else:
|
|
269
|
+
self._handle_yes()
|
|
270
|
+
|
|
258
271
|
event.stop()
|
|
259
272
|
elif event.key == "escape":
|
|
260
273
|
self.dismiss(None)
|
|
@@ -282,18 +295,26 @@ class SaveFileScreen(YesNoScreen):
|
|
|
282
295
|
|
|
283
296
|
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "SaveFileScreen")
|
|
284
297
|
|
|
285
|
-
def __init__(
|
|
298
|
+
def __init__(
|
|
299
|
+
self, filename: str, title: str = "Save to File", all_tabs: bool | None = None, multi_tab: bool = False
|
|
300
|
+
):
|
|
301
|
+
self.all_tabs = all_tabs or (all_tabs is None and multi_tab)
|
|
286
302
|
super().__init__(
|
|
287
303
|
title=title,
|
|
304
|
+
label="Enter filename",
|
|
288
305
|
input=filename,
|
|
306
|
+
yes="Save",
|
|
307
|
+
maybe="Save All Tabs" if self.all_tabs else None,
|
|
308
|
+
no="Cancel",
|
|
289
309
|
on_yes_callback=self.handle_save,
|
|
310
|
+
on_maybe_callback=self.handle_save,
|
|
290
311
|
)
|
|
291
312
|
|
|
292
313
|
def handle_save(self):
|
|
293
314
|
if self.input:
|
|
294
315
|
input_filename = self.input.value.strip()
|
|
295
316
|
if input_filename:
|
|
296
|
-
return input_filename
|
|
317
|
+
return input_filename, self.all_tabs
|
|
297
318
|
else:
|
|
298
319
|
self.notify("Filename cannot be empty", title="Save", severity="error")
|
|
299
320
|
return None
|
|
@@ -587,7 +608,7 @@ class AddColumnScreen(YesNoScreen):
|
|
|
587
608
|
title="Add Column",
|
|
588
609
|
label="Enter column name",
|
|
589
610
|
input="Link" if link else "Column name",
|
|
590
|
-
label2="Enter link template, e.g., https://example.com/$
|
|
611
|
+
label2="Enter link template, e.g., https://example.com/$_/id/$1, PC/compound/$cid"
|
|
591
612
|
if link
|
|
592
613
|
else "Enter value or Polars expression, e.g., abc, pl.lit(123), NULL, $_ * 2, $1 + $total, $_.str.to_uppercase(), pl.concat_str($_, pl.lit('-suffix'))",
|
|
593
614
|
input2="Link template" if link else "Column value or expression",
|
|
@@ -694,3 +715,43 @@ class FindReplaceScreen(YesNoScreen):
|
|
|
694
715
|
replace_all = True
|
|
695
716
|
|
|
696
717
|
return term_find, term_replace, match_nocase, match_whole, replace_all
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
class RenameTabScreen(YesNoScreen):
|
|
721
|
+
"""Modal screen to rename a tab."""
|
|
722
|
+
|
|
723
|
+
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "RenameTabScreen")
|
|
724
|
+
|
|
725
|
+
def __init__(self, content_tab: ContentTab, existing_tabs: list[TabPane]):
|
|
726
|
+
self.content_tab = content_tab
|
|
727
|
+
self.existing_tabs = existing_tabs
|
|
728
|
+
tab_name = content_tab.label_text
|
|
729
|
+
|
|
730
|
+
super().__init__(
|
|
731
|
+
title="Rename Tab",
|
|
732
|
+
label="New tab name",
|
|
733
|
+
input={"value": tab_name},
|
|
734
|
+
on_yes_callback=self._validate_input,
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
def _validate_input(self) -> None:
|
|
738
|
+
"""Validate and save the new tab name."""
|
|
739
|
+
new_name = self.input.value.strip()
|
|
740
|
+
|
|
741
|
+
# Check if name is empty
|
|
742
|
+
if not new_name:
|
|
743
|
+
self.notify("Tab name cannot be empty", title="Rename Tab", severity="error")
|
|
744
|
+
return None
|
|
745
|
+
|
|
746
|
+
# Check if name changed
|
|
747
|
+
if new_name == self.content_tab.label_text:
|
|
748
|
+
self.notify("No changes made", title="Rename Tab", severity="warning")
|
|
749
|
+
return None
|
|
750
|
+
|
|
751
|
+
# Check if name already exists
|
|
752
|
+
if new_name in self.existing_tabs:
|
|
753
|
+
self.notify(f"Tab [$accent]{new_name}[/] already exists", title="Rename Tab", severity="error")
|
|
754
|
+
return None
|
|
755
|
+
|
|
756
|
+
# Return new name
|
|
757
|
+
return self.content_tab, new_name
|