dataframe-textual 2.4.2__tar.gz → 2.5.0__tar.gz
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-2.4.2 → dataframe_textual-2.5.0}/PKG-INFO +7 -8
- {dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/README.md +6 -7
- {dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/pyproject.toml +1 -1
- {dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/src/dataframe_textual/common.py +2 -4
- {dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/src/dataframe_textual/data_frame_table.py +3 -138
- {dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/src/dataframe_textual/data_frame_viewer.py +199 -61
- {dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/uv.lock +1 -1
- {dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/.gitignore +0 -0
- {dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/1811.csv.gz +0 -0
- {dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/LICENSE +0 -0
- {dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/large_malformed.tsv.gz +0 -0
- {dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/main.py +0 -0
- {dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/src/dataframe_textual/__init__.py +0 -0
- {dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/src/dataframe_textual/__main__.py +0 -0
- {dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/src/dataframe_textual/data_frame_help_panel.py +0 -0
- {dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/src/dataframe_textual/sql_screen.py +0 -0
- {dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/src/dataframe_textual/table_screen.py +0 -0
- {dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/src/dataframe_textual/yes_no_screen.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dataframe-textual
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.5.0
|
|
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
|
|
@@ -167,7 +167,7 @@ When multiple files are opened:
|
|
|
167
167
|
## Command Line Options
|
|
168
168
|
|
|
169
169
|
```
|
|
170
|
-
usage: dv [-h] [-V] [-f {csv,json,
|
|
170
|
+
usage: dv [-h] [-V] [-f {csv,json,xlsx,xls,ndjson,psv,parquet,tsv}] [-H] [-I] [-t] [-E] [-c [COMMENT_PREFIX]] [-q [QUOTE_CHAR]] [-l SKIP_LINES] [-a SKIP_ROWS_AFTER_HEADER] [-n NULL [NULL ...]] [files ...]
|
|
171
171
|
|
|
172
172
|
Interactive terminal based viewer/editor for tabular data (e.g., CSV/Excel).
|
|
173
173
|
|
|
@@ -177,8 +177,8 @@ positional arguments:
|
|
|
177
177
|
options:
|
|
178
178
|
-h, --help show this help message and exit
|
|
179
179
|
-V, --version show program's version number and exit
|
|
180
|
-
-f, --format {csv,json,
|
|
181
|
-
Specify the format of the input files (csv,
|
|
180
|
+
-f, --format {csv,json,xlsx,xls,ndjson,psv,parquet,tsv}
|
|
181
|
+
Specify the format of the input files (csv, tsv etc.)
|
|
182
182
|
-H, --no-header Specify that input files have no header row when reading CSV/TSV
|
|
183
183
|
-I, --no-inference Do not infer data types when reading CSV/TSV
|
|
184
184
|
-t, --truncate-ragged-lines
|
|
@@ -250,7 +250,7 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
250
250
|
| `Q` | Close all tabs and app (prompts to save unsaved changes) |
|
|
251
251
|
| `Ctrl+Q` | Force to quit app (regardless of unsaved changes) |
|
|
252
252
|
| `Ctrl+T` | Save current tab to file |
|
|
253
|
-
| `Ctrl+
|
|
253
|
+
| `Ctrl+S` | Save all tabs to file |
|
|
254
254
|
| `w` | Save current tab to file (overwrite without prompt) |
|
|
255
255
|
| `W` | Save all tabs to file (overwrite without prompt) |
|
|
256
256
|
| `Ctrl+D` | Duplicate current tab |
|
|
@@ -391,14 +391,13 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
391
391
|
| `!` | Cast current column to boolean |
|
|
392
392
|
| `$` | Cast current column to string |
|
|
393
393
|
|
|
394
|
-
#### Copy
|
|
394
|
+
#### Copy
|
|
395
395
|
|
|
396
396
|
| Key | Action |
|
|
397
397
|
|-----|--------|
|
|
398
398
|
| `c` | Copy current cell to clipboard |
|
|
399
399
|
| `Ctrl+C` | Copy column to clipboard |
|
|
400
400
|
| `Ctrl+R` | Copy row to clipboard (tab-separated) |
|
|
401
|
-
| `Ctrl+S` | Save to file |
|
|
402
401
|
|
|
403
402
|
## Features in Detail
|
|
404
403
|
|
|
@@ -820,7 +819,7 @@ Manage multiple files and dataframes simultaneously with tabs.
|
|
|
820
819
|
- **`Double-click`** - Rename the tab
|
|
821
820
|
- **`Ctrl+D`** - Duplicate current tab (creates a copy with same data and state)
|
|
822
821
|
- **`Ctrl+T`** - Save current tab to file
|
|
823
|
-
- **`Ctrl+
|
|
822
|
+
- **`Ctrl+S`** - Save all tabs to file
|
|
824
823
|
- **`w`** - Save current tab to file (overwrite without prompt)
|
|
825
824
|
- **`W`** - Save all tabs to file (overwrite without prompt)
|
|
826
825
|
- **`q`** - Close current tab (closes tab, prompts to save if unsaved changes)
|
|
@@ -128,7 +128,7 @@ When multiple files are opened:
|
|
|
128
128
|
## Command Line Options
|
|
129
129
|
|
|
130
130
|
```
|
|
131
|
-
usage: dv [-h] [-V] [-f {csv,json,
|
|
131
|
+
usage: dv [-h] [-V] [-f {csv,json,xlsx,xls,ndjson,psv,parquet,tsv}] [-H] [-I] [-t] [-E] [-c [COMMENT_PREFIX]] [-q [QUOTE_CHAR]] [-l SKIP_LINES] [-a SKIP_ROWS_AFTER_HEADER] [-n NULL [NULL ...]] [files ...]
|
|
132
132
|
|
|
133
133
|
Interactive terminal based viewer/editor for tabular data (e.g., CSV/Excel).
|
|
134
134
|
|
|
@@ -138,8 +138,8 @@ positional arguments:
|
|
|
138
138
|
options:
|
|
139
139
|
-h, --help show this help message and exit
|
|
140
140
|
-V, --version show program's version number and exit
|
|
141
|
-
-f, --format {csv,json,
|
|
142
|
-
Specify the format of the input files (csv,
|
|
141
|
+
-f, --format {csv,json,xlsx,xls,ndjson,psv,parquet,tsv}
|
|
142
|
+
Specify the format of the input files (csv, tsv etc.)
|
|
143
143
|
-H, --no-header Specify that input files have no header row when reading CSV/TSV
|
|
144
144
|
-I, --no-inference Do not infer data types when reading CSV/TSV
|
|
145
145
|
-t, --truncate-ragged-lines
|
|
@@ -211,7 +211,7 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
211
211
|
| `Q` | Close all tabs and app (prompts to save unsaved changes) |
|
|
212
212
|
| `Ctrl+Q` | Force to quit app (regardless of unsaved changes) |
|
|
213
213
|
| `Ctrl+T` | Save current tab to file |
|
|
214
|
-
| `Ctrl+
|
|
214
|
+
| `Ctrl+S` | Save all tabs to file |
|
|
215
215
|
| `w` | Save current tab to file (overwrite without prompt) |
|
|
216
216
|
| `W` | Save all tabs to file (overwrite without prompt) |
|
|
217
217
|
| `Ctrl+D` | Duplicate current tab |
|
|
@@ -352,14 +352,13 @@ zcat compressed_data.csv.gz | dv -f csv
|
|
|
352
352
|
| `!` | Cast current column to boolean |
|
|
353
353
|
| `$` | Cast current column to string |
|
|
354
354
|
|
|
355
|
-
#### Copy
|
|
355
|
+
#### Copy
|
|
356
356
|
|
|
357
357
|
| Key | Action |
|
|
358
358
|
|-----|--------|
|
|
359
359
|
| `c` | Copy current cell to clipboard |
|
|
360
360
|
| `Ctrl+C` | Copy column to clipboard |
|
|
361
361
|
| `Ctrl+R` | Copy row to clipboard (tab-separated) |
|
|
362
|
-
| `Ctrl+S` | Save to file |
|
|
363
362
|
|
|
364
363
|
## Features in Detail
|
|
365
364
|
|
|
@@ -781,7 +780,7 @@ Manage multiple files and dataframes simultaneously with tabs.
|
|
|
781
780
|
- **`Double-click`** - Rename the tab
|
|
782
781
|
- **`Ctrl+D`** - Duplicate current tab (creates a copy with same data and state)
|
|
783
782
|
- **`Ctrl+T`** - Save current tab to file
|
|
784
|
-
- **`Ctrl+
|
|
783
|
+
- **`Ctrl+S`** - Save all tabs to file
|
|
785
784
|
- **`w`** - Save current tab to file (overwrite without prompt)
|
|
786
785
|
- **`W`** - Save all tabs to file (overwrite without prompt)
|
|
787
786
|
- **`q`** - Close current tab (closes tab, prompts to save if unsaved changes)
|
|
@@ -12,7 +12,7 @@ import polars as pl
|
|
|
12
12
|
from rich.text import Text
|
|
13
13
|
|
|
14
14
|
# Supported file formats
|
|
15
|
-
SUPPORTED_FORMATS = ["tsv", "csv", "psv", "
|
|
15
|
+
SUPPORTED_FORMATS = ["tsv", "csv", "psv", "xlsx", "xls", "parquet", "json", "ndjson"]
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
# Boolean string mappings
|
|
@@ -531,8 +531,6 @@ def load_dataframe(
|
|
|
531
531
|
ext = Path(filename).with_suffix("").suffix.lower()
|
|
532
532
|
|
|
533
533
|
fmt = ext.removeprefix(".")
|
|
534
|
-
if fmt in ("xls", "xlsx"):
|
|
535
|
-
fmt = "excel"
|
|
536
534
|
|
|
537
535
|
# Default to TSV
|
|
538
536
|
if not fmt or fmt not in SUPPORTED_FORMATS:
|
|
@@ -688,7 +686,7 @@ def load_file(
|
|
|
688
686
|
truncate_ragged_lines=truncate_ragged_lines,
|
|
689
687
|
)
|
|
690
688
|
data.append(Source(lf, filename, filepath.stem))
|
|
691
|
-
elif file_format
|
|
689
|
+
elif file_format in ("xlsx", "xls"):
|
|
692
690
|
if first_sheet:
|
|
693
691
|
# Read only the first sheet for multiple files
|
|
694
692
|
lf = pl.read_excel(source).lazy()
|
{dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/src/dataframe_textual/data_frame_table.py
RENAMED
|
@@ -16,7 +16,7 @@ from textual.coordinate import Coordinate
|
|
|
16
16
|
from textual.events import Click
|
|
17
17
|
from textual.reactive import reactive
|
|
18
18
|
from textual.render import measure
|
|
19
|
-
from textual.widgets import DataTable
|
|
19
|
+
from textual.widgets import DataTable
|
|
20
20
|
from textual.widgets._data_table import (
|
|
21
21
|
CellDoesNotExist,
|
|
22
22
|
CellKey,
|
|
@@ -35,7 +35,6 @@ from .common import (
|
|
|
35
35
|
NULL_DISPLAY,
|
|
36
36
|
RID,
|
|
37
37
|
SUBSCRIPT_DIGITS,
|
|
38
|
-
SUPPORTED_FORMATS,
|
|
39
38
|
DtypeConfig,
|
|
40
39
|
format_row,
|
|
41
40
|
get_next_item,
|
|
@@ -56,7 +55,6 @@ from .yes_no_screen import (
|
|
|
56
55
|
FindReplaceScreen,
|
|
57
56
|
FreezeScreen,
|
|
58
57
|
RenameColumnScreen,
|
|
59
|
-
SaveFileScreen,
|
|
60
58
|
SearchScreen,
|
|
61
59
|
)
|
|
62
60
|
|
|
@@ -223,11 +221,10 @@ class DataFrameTable(DataTable):
|
|
|
223
221
|
- **!** - ✅ Cast column to boolean
|
|
224
222
|
- **$** - 📝 Cast column to string
|
|
225
223
|
|
|
226
|
-
## 💾 Copy
|
|
224
|
+
## 💾 Copy
|
|
227
225
|
- **c** - 📋 Copy cell to clipboard
|
|
228
226
|
- **Ctrl+c** - 📊 Copy column to clipboard
|
|
229
227
|
- **Ctrl+r** - 📝 Copy row to clipboard (tab-separated)
|
|
230
|
-
- **Ctrl+s** - 💾 Save current tab to file
|
|
231
228
|
""").strip()
|
|
232
229
|
|
|
233
230
|
# fmt: off
|
|
@@ -255,8 +252,6 @@ class DataFrameTable(DataTable):
|
|
|
255
252
|
("c", "copy_cell", "Copy cell to clipboard"),
|
|
256
253
|
("ctrl+c", "copy_column", "Copy column to clipboard"),
|
|
257
254
|
("ctrl+r", "copy_row", "Copy row to clipboard"),
|
|
258
|
-
# Save
|
|
259
|
-
("ctrl+s", "save_to_file", "Save to file"),
|
|
260
255
|
# Metadata, Detail, Frequency, and Statistics
|
|
261
256
|
("m", "metadata_shape", "Show metadata for row count and column count"),
|
|
262
257
|
("M", "metadata_column", "Show metadata for column"),
|
|
@@ -744,10 +739,6 @@ class DataFrameTable(DataTable):
|
|
|
744
739
|
"""Sort by current column in descending order."""
|
|
745
740
|
self.do_sort_by_column(descending=True)
|
|
746
741
|
|
|
747
|
-
def action_save_to_file(self) -> None:
|
|
748
|
-
"""Save the current dataframe to a file."""
|
|
749
|
-
self.do_save_to_file()
|
|
750
|
-
|
|
751
742
|
def action_show_frequency(self) -> None:
|
|
752
743
|
"""Show frequency distribution for the current column."""
|
|
753
744
|
self.do_show_frequency()
|
|
@@ -3755,7 +3746,7 @@ class DataFrameTable(DataTable):
|
|
|
3755
3746
|
|
|
3756
3747
|
self.notify(f"{message}. Now showing [$success]{len(self.df)}[/] rows.", title="Filter Rows")
|
|
3757
3748
|
|
|
3758
|
-
# Copy
|
|
3749
|
+
# Copy
|
|
3759
3750
|
def do_copy_to_clipboard(self, content: str, message: str) -> None:
|
|
3760
3751
|
"""Copy content to clipboard using pbcopy (macOS) or xclip (Linux).
|
|
3761
3752
|
|
|
@@ -3779,132 +3770,6 @@ class DataFrameTable(DataTable):
|
|
|
3779
3770
|
except FileNotFoundError:
|
|
3780
3771
|
self.notify("Error copying to clipboard", title="Copy to Clipboard", severity="error", timeout=10)
|
|
3781
3772
|
|
|
3782
|
-
def do_save_to_file(self, all_tabs: bool | None = None, task_after_save: str | None = None) -> None:
|
|
3783
|
-
"""Open screen to save file."""
|
|
3784
|
-
self._task_after_save = task_after_save
|
|
3785
|
-
tab_count = len(self.app.tabs)
|
|
3786
|
-
save_all = all_tabs is not False
|
|
3787
|
-
|
|
3788
|
-
filepath = Path(self.filename)
|
|
3789
|
-
if save_all:
|
|
3790
|
-
ext = filepath.suffix.lower()
|
|
3791
|
-
if ext in (".xlsx", ".xls"):
|
|
3792
|
-
filename = self.filename
|
|
3793
|
-
else:
|
|
3794
|
-
filename = "all-tabs.xlsx"
|
|
3795
|
-
else:
|
|
3796
|
-
filename = str(filepath.with_stem(self.tabname))
|
|
3797
|
-
|
|
3798
|
-
self.app.push_screen(
|
|
3799
|
-
SaveFileScreen(filename, save_all=save_all, tab_count=tab_count),
|
|
3800
|
-
callback=self.save_to_file,
|
|
3801
|
-
)
|
|
3802
|
-
|
|
3803
|
-
def save_to_file(self, result) -> None:
|
|
3804
|
-
"""Handle result from SaveFileScreen."""
|
|
3805
|
-
if result is None:
|
|
3806
|
-
return
|
|
3807
|
-
filename, save_all, overwrite_prompt = result
|
|
3808
|
-
self._save_all = save_all
|
|
3809
|
-
|
|
3810
|
-
# Check if file exists
|
|
3811
|
-
if overwrite_prompt and Path(filename).exists():
|
|
3812
|
-
self._pending_filename = filename
|
|
3813
|
-
self.app.push_screen(
|
|
3814
|
-
ConfirmScreen("File already exists. Overwrite?"),
|
|
3815
|
-
callback=self.confirm_overwrite,
|
|
3816
|
-
)
|
|
3817
|
-
else:
|
|
3818
|
-
self.save_file(filename)
|
|
3819
|
-
|
|
3820
|
-
def confirm_overwrite(self, should_overwrite: bool) -> None:
|
|
3821
|
-
"""Handle result from ConfirmScreen."""
|
|
3822
|
-
if should_overwrite:
|
|
3823
|
-
self.save_file(self._pending_filename)
|
|
3824
|
-
else:
|
|
3825
|
-
# Go back to SaveFileScreen to allow user to enter a different name
|
|
3826
|
-
self.app.push_screen(
|
|
3827
|
-
SaveFileScreen(self._pending_filename, save_all=self._save_all),
|
|
3828
|
-
callback=self.save_to_file,
|
|
3829
|
-
)
|
|
3830
|
-
|
|
3831
|
-
def save_file(self, filename: str) -> None:
|
|
3832
|
-
"""Actually save the dataframe to a file."""
|
|
3833
|
-
filepath = Path(filename)
|
|
3834
|
-
ext = filepath.suffix.lower()
|
|
3835
|
-
if ext == ".gz":
|
|
3836
|
-
ext = Path(filename).with_suffix("").suffix.lower()
|
|
3837
|
-
|
|
3838
|
-
fmt = ext.removeprefix(".")
|
|
3839
|
-
if fmt not in SUPPORTED_FORMATS:
|
|
3840
|
-
self.notify(
|
|
3841
|
-
f"Unsupported file format [$success]{fmt}[/]. Use [$accent]CSV[/] as fallback. Supported formats: {', '.join(SUPPORTED_FORMATS)}",
|
|
3842
|
-
title="Save to File",
|
|
3843
|
-
severity="warning",
|
|
3844
|
-
)
|
|
3845
|
-
fmt = "csv"
|
|
3846
|
-
|
|
3847
|
-
df = (self.df if self.df_view is None else self.df_view).select(pl.exclude(RID))
|
|
3848
|
-
try:
|
|
3849
|
-
if fmt == "csv":
|
|
3850
|
-
df.write_csv(filename)
|
|
3851
|
-
elif fmt in ("tsv", "tab"):
|
|
3852
|
-
df.write_csv(filename, separator="\t")
|
|
3853
|
-
elif fmt in ("xlsx", "xls"):
|
|
3854
|
-
self.save_excel(filename)
|
|
3855
|
-
elif fmt == "json":
|
|
3856
|
-
df.write_json(filename)
|
|
3857
|
-
elif fmt == "ndjson":
|
|
3858
|
-
df.write_ndjson(filename)
|
|
3859
|
-
elif fmt == "parquet":
|
|
3860
|
-
df.write_parquet(filename)
|
|
3861
|
-
else: # Fallback to CSV
|
|
3862
|
-
df.write_csv(filename)
|
|
3863
|
-
|
|
3864
|
-
# Update current filename
|
|
3865
|
-
self.filename = filename
|
|
3866
|
-
|
|
3867
|
-
# Reset dirty flag after save
|
|
3868
|
-
if self._save_all:
|
|
3869
|
-
tabs: dict[TabPane, DataFrameTable] = self.app.tabs
|
|
3870
|
-
for table in tabs.values():
|
|
3871
|
-
table.dirty = False
|
|
3872
|
-
else:
|
|
3873
|
-
self.dirty = False
|
|
3874
|
-
|
|
3875
|
-
if hasattr(self, "_task_after_save"):
|
|
3876
|
-
if self._task_after_save == "close_tab":
|
|
3877
|
-
self.app.do_close_tab()
|
|
3878
|
-
elif self._task_after_save == "quit_app":
|
|
3879
|
-
self.app.exit()
|
|
3880
|
-
|
|
3881
|
-
# From ConfirmScreen callback, so notify accordingly
|
|
3882
|
-
if self._save_all:
|
|
3883
|
-
self.notify(f"Saved all tabs to [$success]{filename}[/]", title="Save to File")
|
|
3884
|
-
else:
|
|
3885
|
-
self.notify(f"Saved current tab to [$success]{filename}[/]", title="Save to File")
|
|
3886
|
-
|
|
3887
|
-
except Exception as e:
|
|
3888
|
-
self.notify(f"Error saving [$error]{filename}[/]", title="Save to File", severity="error", timeout=10)
|
|
3889
|
-
self.log(f"Error saving file `{filename}`: {str(e)}")
|
|
3890
|
-
|
|
3891
|
-
def save_excel(self, filename: str) -> None:
|
|
3892
|
-
"""Save to an Excel file."""
|
|
3893
|
-
import xlsxwriter
|
|
3894
|
-
|
|
3895
|
-
if not self._save_all or len(self.app.tabs) == 1:
|
|
3896
|
-
# Single tab - save directly
|
|
3897
|
-
df = (self.df if self.df_view is None else self.df_view).select(pl.exclude(RID))
|
|
3898
|
-
df.write_excel(filename, worksheet=self.tabname)
|
|
3899
|
-
else:
|
|
3900
|
-
# Multiple tabs - use xlsxwriter to create multiple sheets
|
|
3901
|
-
with xlsxwriter.Workbook(filename) as wb:
|
|
3902
|
-
tabs: dict[TabPane, DataFrameTable] = self.app.tabs
|
|
3903
|
-
for table in tabs.values():
|
|
3904
|
-
worksheet = wb.add_worksheet(table.tabname)
|
|
3905
|
-
df = (table.df if table.df_view is None else table.df_view).select(pl.exclude(RID))
|
|
3906
|
-
df.write_excel(workbook=wb, worksheet=worksheet)
|
|
3907
|
-
|
|
3908
3773
|
# SQL Interface
|
|
3909
3774
|
def do_simple_sql(self) -> None:
|
|
3910
3775
|
"""Open the SQL interface screen."""
|
{dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/src/dataframe_textual/data_frame_viewer.py
RENAMED
|
@@ -12,10 +12,10 @@ from textual.theme import BUILTIN_THEMES
|
|
|
12
12
|
from textual.widgets import TabbedContent, TabPane
|
|
13
13
|
from textual.widgets.tabbed_content import ContentTab, ContentTabs
|
|
14
14
|
|
|
15
|
-
from .common import Source, get_next_item, load_file
|
|
15
|
+
from .common import RID, SUPPORTED_FORMATS, Source, get_next_item, load_file
|
|
16
16
|
from .data_frame_help_panel import DataFrameHelpPanel
|
|
17
17
|
from .data_frame_table import DataFrameTable
|
|
18
|
-
from .yes_no_screen import ConfirmScreen, OpenFileScreen, RenameTabScreen
|
|
18
|
+
from .yes_no_screen import ConfirmScreen, OpenFileScreen, RenameTabScreen, SaveFileScreen
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class DataFrameViewer(App):
|
|
@@ -34,7 +34,7 @@ class DataFrameViewer(App):
|
|
|
34
34
|
- **Ctrl+Q** - 🚪 Force to quit app (discards unsaved changes)
|
|
35
35
|
- **Ctrl+T** - 💾 Save current tab to file
|
|
36
36
|
- **w** - 💾 Save current tab to file (overwrite without prompt)
|
|
37
|
-
- **Ctrl+
|
|
37
|
+
- **Ctrl+S** - 💾 Save all tabs to file
|
|
38
38
|
- **W** - 💾 Save all tabs to file (overwrite without prompt)
|
|
39
39
|
- **Ctrl+D** - 📋 Duplicate current tab
|
|
40
40
|
- **Ctrl+O** - 📁 Open a file
|
|
@@ -64,7 +64,7 @@ class DataFrameViewer(App):
|
|
|
64
64
|
("f1", "toggle_help_panel", "Help"),
|
|
65
65
|
("ctrl+o", "open_file", "Open File"),
|
|
66
66
|
("ctrl+t", "save_current_tab", "Save Current Tab"),
|
|
67
|
-
("ctrl+
|
|
67
|
+
("ctrl+s", "save_all_tabs", "Save All Tabs"),
|
|
68
68
|
("w", "save_current_tab_overwrite", "Save Current Tab (overwrite)"),
|
|
69
69
|
("W", "save_all_tabs_overwrite", "Save All Tabs (overwrite)"),
|
|
70
70
|
("ctrl+d", "duplicate_tab", "Duplicate Tab"),
|
|
@@ -102,6 +102,22 @@ class DataFrameViewer(App):
|
|
|
102
102
|
self.tabs: dict[TabPane, DataFrameTable] = {}
|
|
103
103
|
self.help_panel = None
|
|
104
104
|
|
|
105
|
+
@property
|
|
106
|
+
def active_table(self) -> DataFrameTable | None:
|
|
107
|
+
"""Get the currently active DataFrameTable widget.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
The active DataFrameTable widget, or None if not found.
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
tabbed: TabbedContent = self.query_one(TabbedContent)
|
|
114
|
+
if active_pane := tabbed.active_pane:
|
|
115
|
+
return active_pane.query_one(DataFrameTable)
|
|
116
|
+
except (NoMatches, AttributeError):
|
|
117
|
+
self.notify("No active table found", title="Locate Table", severity="error", timeout=10)
|
|
118
|
+
|
|
119
|
+
return None
|
|
120
|
+
|
|
105
121
|
def compose(self) -> ComposeResult:
|
|
106
122
|
"""Compose the application widget structure.
|
|
107
123
|
|
|
@@ -151,7 +167,7 @@ class DataFrameViewer(App):
|
|
|
151
167
|
"""
|
|
152
168
|
if len(self.tabs) == 1:
|
|
153
169
|
self.query_one(ContentTabs).display = False
|
|
154
|
-
self.
|
|
170
|
+
self.active_table.focus()
|
|
155
171
|
|
|
156
172
|
def on_ready(self) -> None:
|
|
157
173
|
"""Called when the app is ready."""
|
|
@@ -201,13 +217,11 @@ class DataFrameViewer(App):
|
|
|
201
217
|
event: The tab activated event containing the activated tab pane.
|
|
202
218
|
"""
|
|
203
219
|
# Focus the table in the newly activated tab
|
|
204
|
-
if table := self.
|
|
220
|
+
if table := self.active_table:
|
|
205
221
|
table.focus()
|
|
206
|
-
else:
|
|
207
|
-
return
|
|
208
222
|
|
|
209
|
-
|
|
210
|
-
|
|
223
|
+
if table.loaded_rows == 0:
|
|
224
|
+
table.setup_table()
|
|
211
225
|
|
|
212
226
|
def action_toggle_help_panel(self) -> None:
|
|
213
227
|
"""Toggle the help panel on or off.
|
|
@@ -245,38 +259,43 @@ class DataFrameViewer(App):
|
|
|
245
259
|
self.do_close_all_tabs()
|
|
246
260
|
|
|
247
261
|
def action_save_current_tab(self) -> None:
|
|
248
|
-
"""
|
|
249
|
-
|
|
250
|
-
Opens the save dialog for the active tab's DataFrameTable to save its data.
|
|
251
|
-
"""
|
|
252
|
-
if table := self.get_active_table():
|
|
253
|
-
table.do_save_to_file(all_tabs=False)
|
|
262
|
+
"""Open a save dialog to save current tab to file."""
|
|
263
|
+
self.do_save_to_file(all_tabs=False)
|
|
254
264
|
|
|
255
265
|
def action_save_all_tabs(self) -> None:
|
|
256
|
-
"""
|
|
257
|
-
|
|
258
|
-
Iterates through all DataFrameTable widgets and opens the save dialog for each.
|
|
259
|
-
"""
|
|
260
|
-
if table := self.get_active_table():
|
|
261
|
-
table.do_save_to_file(all_tabs=True)
|
|
266
|
+
"""Open a save dialog to save all tabs to file."""
|
|
267
|
+
self.do_save_to_file(all_tabs=True)
|
|
262
268
|
|
|
263
269
|
def action_save_current_tab_overwrite(self) -> None:
|
|
264
|
-
"""Save
|
|
265
|
-
if table := self.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
270
|
+
"""Save current tab to file, overwrite if exists."""
|
|
271
|
+
if table := self.active_table:
|
|
272
|
+
if len(self.tabs) > 1:
|
|
273
|
+
filenames = {t.filename for t in self.tabs.values()}
|
|
274
|
+
if len(filenames) > 1:
|
|
275
|
+
# Different filenames across tabs
|
|
276
|
+
filepath = Path(table.filename)
|
|
277
|
+
filename = filepath.with_stem(table.tabname)
|
|
278
|
+
else:
|
|
279
|
+
filename = table.filename
|
|
280
|
+
else:
|
|
281
|
+
filename = table.filename
|
|
282
|
+
|
|
283
|
+
self.save_to_file((filename, False, False))
|
|
269
284
|
|
|
270
285
|
def action_save_all_tabs_overwrite(self) -> None:
|
|
271
|
-
"""Save all
|
|
272
|
-
if table := self.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
286
|
+
"""Save all tabs to file, overwrite if exists."""
|
|
287
|
+
if table := self.active_table:
|
|
288
|
+
if len(self.tabs) > 1:
|
|
289
|
+
filenames = {t.filename for t in self.tabs.values()}
|
|
290
|
+
if len(filenames) > 1:
|
|
291
|
+
# Different filenames across tabs - use generic name
|
|
292
|
+
filename = "all-tabs.xlsx"
|
|
293
|
+
else:
|
|
294
|
+
filename = table.filename
|
|
276
295
|
else:
|
|
277
|
-
filename =
|
|
296
|
+
filename = table.filename
|
|
278
297
|
|
|
279
|
-
|
|
298
|
+
self.save_to_file((filename, True, False))
|
|
280
299
|
|
|
281
300
|
def action_duplicate_tab(self) -> None:
|
|
282
301
|
"""Duplicate the currently active tab.
|
|
@@ -292,7 +311,7 @@ class DataFrameViewer(App):
|
|
|
292
311
|
Creates a copy of the current tab with the same data and filename.
|
|
293
312
|
The new tab is named with '_copy' suffix and inserted after the current tab.
|
|
294
313
|
"""
|
|
295
|
-
if not (table := self.
|
|
314
|
+
if not (table := self.active_table):
|
|
296
315
|
return
|
|
297
316
|
|
|
298
317
|
# Get current tab info
|
|
@@ -364,24 +383,6 @@ class DataFrameViewer(App):
|
|
|
364
383
|
# status = "shown" if tabs.display else "hidden"
|
|
365
384
|
# self.notify(f"Tab bar [$success]{status}[/]", title="Toggle Tab Bar")
|
|
366
385
|
|
|
367
|
-
def get_active_table(self) -> DataFrameTable | None:
|
|
368
|
-
"""Get the currently active DataFrameTable widget.
|
|
369
|
-
|
|
370
|
-
Retrieves the table from the currently active tab. Returns None if no
|
|
371
|
-
table is found or an error occurs.
|
|
372
|
-
|
|
373
|
-
Returns:
|
|
374
|
-
The active DataFrameTable widget, or None if not found.
|
|
375
|
-
"""
|
|
376
|
-
try:
|
|
377
|
-
tabbed: TabbedContent = self.query_one(TabbedContent)
|
|
378
|
-
if active_pane := tabbed.active_pane:
|
|
379
|
-
return active_pane.query_one(DataFrameTable)
|
|
380
|
-
except (NoMatches, AttributeError):
|
|
381
|
-
self.notify("No active table found", title="Locate Table", severity="error", timeout=10)
|
|
382
|
-
|
|
383
|
-
return None
|
|
384
|
-
|
|
385
386
|
def get_unique_tabname(self, tab_name: str) -> str:
|
|
386
387
|
"""Generate a unique tab name based on the given base name.
|
|
387
388
|
|
|
@@ -492,17 +493,14 @@ class DataFrameViewer(App):
|
|
|
492
493
|
can be closed, the application exits instead.
|
|
493
494
|
"""
|
|
494
495
|
try:
|
|
495
|
-
if not (
|
|
496
|
-
return
|
|
497
|
-
|
|
498
|
-
if not (active_table := self.tabs.get(active_pane)):
|
|
496
|
+
if not (table := self.active_table):
|
|
499
497
|
return
|
|
500
498
|
|
|
501
499
|
def _on_save_confirm(result: bool) -> None:
|
|
502
500
|
"""Handle the "save before closing?" confirmation."""
|
|
503
501
|
if result:
|
|
504
502
|
# User wants to save - close after save dialog opens
|
|
505
|
-
|
|
503
|
+
self.do_save_to_file(all_tabs=False, task_after_save="close_tab")
|
|
506
504
|
elif result is None:
|
|
507
505
|
# User cancelled - do nothing
|
|
508
506
|
return
|
|
@@ -510,7 +508,7 @@ class DataFrameViewer(App):
|
|
|
510
508
|
# User wants to discard - close immediately
|
|
511
509
|
self.close_tab()
|
|
512
510
|
|
|
513
|
-
if
|
|
511
|
+
if table.dirty:
|
|
514
512
|
self.push_screen(
|
|
515
513
|
ConfirmScreen(
|
|
516
514
|
"Close Tab",
|
|
@@ -557,7 +555,7 @@ class DataFrameViewer(App):
|
|
|
557
555
|
|
|
558
556
|
def _save_and_quit(result: bool) -> None:
|
|
559
557
|
if result:
|
|
560
|
-
self.
|
|
558
|
+
self.do_save_to_file(all_tabs=True, task_after_save="quit_app")
|
|
561
559
|
elif result is None:
|
|
562
560
|
# User cancelled - do nothing
|
|
563
561
|
return
|
|
@@ -565,15 +563,17 @@ class DataFrameViewer(App):
|
|
|
565
563
|
# User wants to discard - quit immediately
|
|
566
564
|
self.exit()
|
|
567
565
|
|
|
566
|
+
tab_count = len(self.tabs)
|
|
568
567
|
tab_list = "\n".join(f" - [$warning]{name}[/]" for name in dirty_tabnames)
|
|
569
568
|
label = (
|
|
570
569
|
f"The following tabs have unsaved changes:\n\n{tab_list}\n\nSave all changes?"
|
|
571
570
|
if len(dirty_tabnames) > 1
|
|
572
571
|
else f"The tab [$warning]{dirty_tabnames[0]}[/] has unsaved changes.\n\nSave changes?"
|
|
573
572
|
)
|
|
573
|
+
|
|
574
574
|
self.push_screen(
|
|
575
575
|
ConfirmScreen(
|
|
576
|
-
"Close
|
|
576
|
+
f"Close {tab_count} Tabs" if tab_count > 1 else "Close Tab",
|
|
577
577
|
label=label,
|
|
578
578
|
yes="Save",
|
|
579
579
|
maybe="Discard",
|
|
@@ -628,3 +628,141 @@ class DataFrameViewer(App):
|
|
|
628
628
|
break
|
|
629
629
|
|
|
630
630
|
# self.notify(f"Renamed tab [$accent]{old_name}[/] to [$success]{new_name}[/]", title="Rename Tab")
|
|
631
|
+
|
|
632
|
+
def do_save_to_file(self, all_tabs: bool = True, task_after_save: str | None = None) -> None:
|
|
633
|
+
"""Open screen to save file."""
|
|
634
|
+
if not (table := self.active_table):
|
|
635
|
+
return
|
|
636
|
+
|
|
637
|
+
self._task_after_save = task_after_save
|
|
638
|
+
tab_count = len(self.tabs)
|
|
639
|
+
save_all = all_tabs is True and tab_count > 1
|
|
640
|
+
|
|
641
|
+
if save_all:
|
|
642
|
+
filenames = {t.filename for t in self.tabs.values()}
|
|
643
|
+
if len(filenames) > 1:
|
|
644
|
+
# Different filenames across tabs - use generic name
|
|
645
|
+
filename = "all-tabs.xlsx"
|
|
646
|
+
else:
|
|
647
|
+
filename = table.filename
|
|
648
|
+
elif tab_count == 1:
|
|
649
|
+
filename = table.filename
|
|
650
|
+
else:
|
|
651
|
+
filepath = Path(table.filename)
|
|
652
|
+
filename = str(filepath.with_stem(table.tabname))
|
|
653
|
+
|
|
654
|
+
self.push_screen(
|
|
655
|
+
SaveFileScreen(filename, save_all=save_all, tab_count=tab_count),
|
|
656
|
+
callback=self.save_to_file,
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
def save_to_file(self, result) -> None:
|
|
660
|
+
"""Handle result from SaveFileScreen."""
|
|
661
|
+
if result is None:
|
|
662
|
+
return
|
|
663
|
+
filename, save_all, overwrite_prompt = result
|
|
664
|
+
self._save_all = save_all
|
|
665
|
+
|
|
666
|
+
# Check if file exists
|
|
667
|
+
if overwrite_prompt and Path(filename).exists():
|
|
668
|
+
self._pending_filename = filename
|
|
669
|
+
self.push_screen(
|
|
670
|
+
ConfirmScreen("File already exists. Overwrite?"),
|
|
671
|
+
callback=self.confirm_overwrite,
|
|
672
|
+
)
|
|
673
|
+
else:
|
|
674
|
+
self.save_file(filename)
|
|
675
|
+
|
|
676
|
+
def confirm_overwrite(self, should_overwrite: bool) -> None:
|
|
677
|
+
"""Handle result from ConfirmScreen."""
|
|
678
|
+
if should_overwrite:
|
|
679
|
+
self.save_file(self._pending_filename)
|
|
680
|
+
else:
|
|
681
|
+
# Go back to SaveFileScreen to allow user to enter a different name
|
|
682
|
+
self.push_screen(
|
|
683
|
+
SaveFileScreen(self._pending_filename, save_all=self._save_all),
|
|
684
|
+
callback=self.save_to_file,
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
def save_file(self, filename: str) -> None:
|
|
688
|
+
"""Actually save to a file."""
|
|
689
|
+
if not (table := self.active_table):
|
|
690
|
+
return
|
|
691
|
+
|
|
692
|
+
filepath = Path(filename)
|
|
693
|
+
ext = filepath.suffix.lower()
|
|
694
|
+
if ext == ".gz":
|
|
695
|
+
ext = Path(filename).with_suffix("").suffix.lower()
|
|
696
|
+
|
|
697
|
+
fmt = ext.removeprefix(".")
|
|
698
|
+
if fmt not in SUPPORTED_FORMATS:
|
|
699
|
+
self.notify(
|
|
700
|
+
f"Unsupported file format [$success]{fmt}[/]. Use [$accent]CSV[/] as fallback. Supported formats: {', '.join(SUPPORTED_FORMATS)}",
|
|
701
|
+
title="Save to File",
|
|
702
|
+
severity="warning",
|
|
703
|
+
)
|
|
704
|
+
fmt = "csv"
|
|
705
|
+
|
|
706
|
+
df = (table.df if table.df_view is None else table.df_view).select(pl.exclude(RID))
|
|
707
|
+
try:
|
|
708
|
+
if fmt == "csv":
|
|
709
|
+
df.write_csv(filename)
|
|
710
|
+
elif fmt in ("tsv", "tab"):
|
|
711
|
+
df.write_csv(filename, separator="\t")
|
|
712
|
+
elif fmt == "psv":
|
|
713
|
+
df.write_csv(filename, separator="|")
|
|
714
|
+
elif fmt in ("xlsx", "xls"):
|
|
715
|
+
self.save_excel(filename)
|
|
716
|
+
elif fmt == "json":
|
|
717
|
+
df.write_json(filename)
|
|
718
|
+
elif fmt == "ndjson":
|
|
719
|
+
df.write_ndjson(filename)
|
|
720
|
+
elif fmt == "parquet":
|
|
721
|
+
df.write_parquet(filename)
|
|
722
|
+
else: # Fallback to CSV
|
|
723
|
+
df.write_csv(filename)
|
|
724
|
+
|
|
725
|
+
# Reset dirty flag and update filename after save
|
|
726
|
+
if self._save_all:
|
|
727
|
+
for table in self.tabs.values():
|
|
728
|
+
table.dirty = False
|
|
729
|
+
table.filename = filename
|
|
730
|
+
else:
|
|
731
|
+
table.dirty = False
|
|
732
|
+
table.filename = filename
|
|
733
|
+
|
|
734
|
+
# From ConfirmScreen callback, so notify accordingly
|
|
735
|
+
if self._save_all:
|
|
736
|
+
self.notify(f"Saved all tabs to [$success]{filename}[/]", title="Save to File")
|
|
737
|
+
else:
|
|
738
|
+
self.notify(f"Saved current tab to [$success]{filename}[/]", title="Save to File")
|
|
739
|
+
|
|
740
|
+
if hasattr(self, "_task_after_save"):
|
|
741
|
+
if self._task_after_save == "close_tab":
|
|
742
|
+
self.do_close_tab()
|
|
743
|
+
elif self._task_after_save == "quit_app":
|
|
744
|
+
self.exit()
|
|
745
|
+
|
|
746
|
+
except Exception as e:
|
|
747
|
+
self.notify(f"Error saving [$error]{filename}[/]", title="Save to File", severity="error", timeout=10)
|
|
748
|
+
self.log(f"Error saving file `{filename}`: {str(e)}")
|
|
749
|
+
|
|
750
|
+
def save_excel(self, filename: str) -> None:
|
|
751
|
+
"""Save to an Excel file."""
|
|
752
|
+
import xlsxwriter
|
|
753
|
+
|
|
754
|
+
if not self._save_all or len(self.tabs) == 1:
|
|
755
|
+
# Single tab - save directly
|
|
756
|
+
if not (table := self.active_table):
|
|
757
|
+
return
|
|
758
|
+
|
|
759
|
+
df = (table.df if table.df_view is None else table.df_view).select(pl.exclude(RID))
|
|
760
|
+
df.write_excel(filename, worksheet=table.tabname)
|
|
761
|
+
else:
|
|
762
|
+
# Multiple tabs - use xlsxwriter to create multiple sheets
|
|
763
|
+
with xlsxwriter.Workbook(filename) as wb:
|
|
764
|
+
tabs: dict[TabPane, DataFrameTable] = self.tabs
|
|
765
|
+
for table in tabs.values():
|
|
766
|
+
worksheet = wb.add_worksheet(table.tabname)
|
|
767
|
+
df = (table.df if table.df_view is None else table.df_view).select(pl.exclude(RID))
|
|
768
|
+
df.write_excel(workbook=wb, worksheet=worksheet)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dataframe_textual-2.4.2 → dataframe_textual-2.5.0}/src/dataframe_textual/data_frame_help_panel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|