dataframe-textual 1.4.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 +10 -4
- dataframe_textual/common.py +201 -163
- dataframe_textual/data_frame_table.py +1037 -881
- dataframe_textual/data_frame_viewer.py +321 -104
- dataframe_textual/sql_screen.py +50 -11
- dataframe_textual/table_screen.py +1 -1
- dataframe_textual/yes_no_screen.py +89 -8
- {dataframe_textual-1.4.0.dist-info → dataframe_textual-1.9.0.dist-info}/METADATA +141 -185
- dataframe_textual-1.9.0.dist-info/RECORD +14 -0
- dataframe_textual-1.4.0.dist-info/RECORD +0 -14
- {dataframe_textual-1.4.0.dist-info → dataframe_textual-1.9.0.dist-info}/WHEEL +0 -0
- {dataframe_textual-1.4.0.dist-info → dataframe_textual-1.9.0.dist-info}/entry_points.txt +0 -0
- {dataframe_textual-1.4.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
|
|
@@ -578,16 +599,19 @@ class AddColumnScreen(YesNoScreen):
|
|
|
578
599
|
|
|
579
600
|
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "AddColumnScreen")
|
|
580
601
|
|
|
581
|
-
def __init__(self, cidx: int, df: pl.DataFrame):
|
|
602
|
+
def __init__(self, cidx: int, df: pl.DataFrame, link: bool = False):
|
|
582
603
|
self.cidx = cidx
|
|
583
604
|
self.df = df
|
|
605
|
+
self.link = link
|
|
584
606
|
self.existing_columns = set(df.columns)
|
|
585
607
|
super().__init__(
|
|
586
608
|
title="Add Column",
|
|
587
609
|
label="Enter column name",
|
|
588
|
-
input="
|
|
589
|
-
label2="Enter
|
|
590
|
-
|
|
610
|
+
input="Link" if link else "Column name",
|
|
611
|
+
label2="Enter link template, e.g., https://example.com/$_/id/$1, PC/compound/$cid"
|
|
612
|
+
if link
|
|
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'))",
|
|
614
|
+
input2="Link template" if link else "Column value or expression",
|
|
591
615
|
on_yes_callback=self._get_input,
|
|
592
616
|
)
|
|
593
617
|
|
|
@@ -611,6 +635,9 @@ class AddColumnScreen(YesNoScreen):
|
|
|
611
635
|
|
|
612
636
|
if term == NULL:
|
|
613
637
|
return self.cidx, col_name, pl.lit(None)
|
|
638
|
+
elif self.link:
|
|
639
|
+
# Treat as link template
|
|
640
|
+
return self.cidx, col_name, term
|
|
614
641
|
elif tentative_expr(term):
|
|
615
642
|
try:
|
|
616
643
|
expr = validate_expr(term, self.df.columns, self.cidx)
|
|
@@ -633,6 +660,20 @@ class AddColumnScreen(YesNoScreen):
|
|
|
633
660
|
return self.cidx, col_name, pl.lit(term)
|
|
634
661
|
|
|
635
662
|
|
|
663
|
+
class AddLinkScreen(AddColumnScreen):
|
|
664
|
+
"""Modal screen to add a new link column with user-provided expressions.
|
|
665
|
+
|
|
666
|
+
Allows user to specify a column name and a value or Polars expression that will be
|
|
667
|
+
evaluated to create links. A new column is created with the resulting link values.
|
|
668
|
+
Inherits column name and expression validation from AddColumnScreen.
|
|
669
|
+
"""
|
|
670
|
+
|
|
671
|
+
CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "AddLinkScreen")
|
|
672
|
+
|
|
673
|
+
def __init__(self, cidx: int, df: pl.DataFrame):
|
|
674
|
+
super().__init__(cidx, df, link=True)
|
|
675
|
+
|
|
676
|
+
|
|
636
677
|
class FindReplaceScreen(YesNoScreen):
|
|
637
678
|
"""Modal screen to replace column values with an expression."""
|
|
638
679
|
|
|
@@ -674,3 +715,43 @@ class FindReplaceScreen(YesNoScreen):
|
|
|
674
715
|
replace_all = True
|
|
675
716
|
|
|
676
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
|