dataframe-textual 1.16.2__py3-none-any.whl → 2.0.1__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.
@@ -248,12 +248,7 @@ class DataFrameViewer(App):
248
248
  Opens the save dialog for the active tab's DataFrameTable to save its data.
249
249
  """
250
250
  if table := self.get_active_table():
251
- table.do_save_to_file(title="Save Current Tab", all_tabs=False)
252
-
253
- def action_save_current_tab_overwrite(self) -> None:
254
- """Save the currently active tab to file, overwriting if it exists."""
255
- if table := self.get_active_table():
256
- table.save_to_file((table.filename, False, False))
251
+ table.do_save_to_file(all_tabs=False)
257
252
 
258
253
  def action_save_all_tabs(self) -> None:
259
254
  """Save all open tabs to their respective files.
@@ -261,12 +256,25 @@ class DataFrameViewer(App):
261
256
  Iterates through all DataFrameTable widgets and opens the save dialog for each.
262
257
  """
263
258
  if table := self.get_active_table():
264
- table.do_save_to_file(title="Save All Tabs", all_tabs=True)
259
+ table.do_save_to_file(all_tabs=True)
260
+
261
+ def action_save_current_tab_overwrite(self) -> None:
262
+ """Save the currently active tab to file, overwriting if it exists."""
263
+ if table := self.get_active_table():
264
+ filepath = Path(table.filename)
265
+ filename = filepath.with_stem(table.tabname)
266
+ table.save_to_file((filename, False, False))
265
267
 
266
268
  def action_save_all_tabs_overwrite(self) -> None:
267
269
  """Save all open tabs to their respective files, overwriting if they exist."""
268
270
  if table := self.get_active_table():
269
- table.save_to_file((table.filename, True, False))
271
+ filepath = Path(table.filename)
272
+ if filepath.suffix.lower() in [".xlsx", ".xls"]:
273
+ filename = table.filename
274
+ else:
275
+ filename = "all-tabs.xlsx"
276
+
277
+ table.save_to_file((filename, True, False))
270
278
 
271
279
  def action_duplicate_tab(self) -> None:
272
280
  """Duplicate the currently active tab.
@@ -13,6 +13,8 @@ from textual.screen import ModalScreen
13
13
  from textual.widgets import Button, Input, Label, SelectionList, TextArea
14
14
  from textual.widgets.selection_list import Selection
15
15
 
16
+ from .common import RID
17
+
16
18
 
17
19
  class SqlScreen(ModalScreen):
18
20
  """Base class for modal screens handling SQL query."""
@@ -164,7 +166,11 @@ class SimpleSqlScreen(SqlScreen):
164
166
  container.border_title = "SQL Query"
165
167
  yield Label("SELECT columns (all if none selected):", id="select-label")
166
168
  yield SelectionList(
167
- *[Selection(col, col) for col in self.df.columns if col not in self.dftable.hidden_columns],
169
+ *[
170
+ Selection(col, col)
171
+ for col in self.df.columns
172
+ if col not in self.dftable.hidden_columns and col != RID
173
+ ],
168
174
  id="column-selection",
169
175
  )
170
176
  yield Label("WHERE condition (optional)", id="where-label")
@@ -175,7 +181,7 @@ class SimpleSqlScreen(SqlScreen):
175
181
  """Handle Yes button/Enter key press."""
176
182
  selections = self.query_one(SelectionList).selected
177
183
  if not selections:
178
- selections = [col for col in self.df.columns if col not in self.dftable.hidden_columns]
184
+ selections = [col for col in self.df.columns if col not in self.dftable.hidden_columns and col != RID]
179
185
 
180
186
  columns = ", ".join(f"`{s}`" for s in selections)
181
187
  where = self.query_one(Input).value.strip()
@@ -13,7 +13,7 @@ from textual.renderables.bar import Bar
13
13
  from textual.screen import ModalScreen
14
14
  from textual.widgets import DataTable
15
15
 
16
- from .common import NULL, NULL_DISPLAY, RIDX, DtypeConfig, format_float
16
+ from .common import NULL, NULL_DISPLAY, RID, DtypeConfig, format_float
17
17
 
18
18
 
19
19
  class TableScreen(ModalScreen):
@@ -45,9 +45,6 @@ class TableScreen(ModalScreen):
45
45
 
46
46
  Args:
47
47
  dftable: Reference to the parent DataFrameTable widget.
48
-
49
- Returns:
50
- None
51
48
  """
52
49
  super().__init__()
53
50
  self.dftable = dftable # DataFrameTable
@@ -71,9 +68,6 @@ class TableScreen(ModalScreen):
71
68
 
72
69
  Subclasses should implement this method to populate the DataTable
73
70
  with appropriate columns and rows based on the specific screen's purpose.
74
-
75
- Returns:
76
- None
77
71
  """
78
72
  raise NotImplementedError("Subclasses must implement build_table method.")
79
73
 
@@ -85,9 +79,6 @@ class TableScreen(ModalScreen):
85
79
 
86
80
  Args:
87
81
  event: The key event object.
88
-
89
- Returns:
90
- None
91
82
  """
92
83
  if event.key in ("q", "escape"):
93
84
  self.app.pop_screen()
@@ -111,7 +102,7 @@ class TableScreen(ModalScreen):
111
102
  if cidx_name_value is None:
112
103
  return
113
104
  cidx, col_name, col_value = cidx_name_value
114
- self.log(f"Filtering or viewing by {col_name} == {col_value}")
105
+ self.log(f"Filtering or viewing by `{col_name} == {col_value}`")
115
106
 
116
107
  # Handle NULL values
117
108
  if col_value == NULL:
@@ -123,11 +114,11 @@ class TableScreen(ModalScreen):
123
114
  expr = pl.col(col_name) == col_value
124
115
  value_display = f"[$success]{col_value}[/]"
125
116
 
126
- df_filtered = self.dftable.df.with_row_index(RIDX).filter(expr)
117
+ df_filtered = self.dftable.df.lazy().filter(expr).collect()
127
118
  self.log(f"Filtered dataframe has {len(df_filtered)} rows")
128
119
 
129
- matched_indices = set(df_filtered[RIDX].to_list())
130
- if not matched_indices:
120
+ ok_rids = set(df_filtered[RID].to_list())
121
+ if not ok_rids:
131
122
  self.notify(
132
123
  f"No matches found for [$warning]{col_name}[/] == {value_display}",
133
124
  title="No Matches",
@@ -135,18 +126,12 @@ class TableScreen(ModalScreen):
135
126
  )
136
127
  return
137
128
 
138
- # Apply the action
129
+ # Action filter
139
130
  if action == "filter":
140
- # Update selections
141
- for i in range(len(self.dftable.selected_rows)):
142
- self.dftable.selected_rows[i] = i in matched_indices
143
-
144
- # Update main table display
145
131
  self.dftable.do_filter_rows()
146
132
 
147
- else: # action == "view"
148
- # Update visible rows
149
- expr = [i in matched_indices for i in range(len(self.dftable.df))]
133
+ # Action view
134
+ else:
150
135
  self.dftable.view_rows((expr, cidx, False, True))
151
136
 
152
137
  # Dismiss the frequency screen
@@ -167,9 +152,6 @@ class RowDetailScreen(TableScreen):
167
152
 
168
153
  Populates the table with column names and values from the selected row
169
154
  of the main DataFrame. Sets the table cursor type to "row".
170
-
171
- Returns:
172
- None
173
155
  """
174
156
  self.build_table()
175
157
 
@@ -181,6 +163,8 @@ class RowDetailScreen(TableScreen):
181
163
 
182
164
  # Get all columns and values from the dataframe row
183
165
  for col, val, dtype in zip(self.df.columns, self.df.row(self.ridx), self.df.dtypes):
166
+ if col == RID:
167
+ continue # Skip RID column
184
168
  formatted_row = []
185
169
  formatted_row.append(col)
186
170
 
@@ -208,20 +192,16 @@ class RowDetailScreen(TableScreen):
208
192
  self.filter_or_view_selected_value(self.get_cidx_name_value(), action="filter")
209
193
  event.stop()
210
194
  elif event.key == "right_curly_bracket": # '}'
211
- # Move to the next visible row
195
+ # Move to the next row
212
196
  ridx = self.ridx + 1
213
- while ridx < len(self.df) and not self.dftable.visible_rows[ridx]:
214
- ridx += 1
215
197
  if ridx < len(self.df):
216
198
  self.ridx = ridx
217
199
  self.dftable.move_cursor_to(self.ridx)
218
200
  self.build_table()
219
201
  event.stop()
220
202
  elif event.key == "left_curly_bracket": # '{'
221
- # Move to the previous visible row
203
+ # Move to the previous row
222
204
  ridx = self.ridx - 1
223
- while ridx >= 0 and not self.dftable.visible_rows[ridx]:
224
- ridx -= 1
225
205
  if ridx >= 0:
226
206
  self.ridx = ridx
227
207
  self.dftable.move_cursor_to(self.ridx)
@@ -270,12 +250,8 @@ class StatisticsScreen(TableScreen):
270
250
  col_name = self.df.columns[self.col_idx]
271
251
  lf = self.df.lazy()
272
252
 
273
- # Apply only to visible rows
274
- if False in self.dftable.visible_rows:
275
- lf = lf.filter(self.dftable.visible_rows)
276
-
277
253
  # Get column statistics
278
- stats_df = lf.select(pl.col(col_name)).collect().describe()
254
+ stats_df = lf.select(pl.col(col_name)).describe()
279
255
  if len(stats_df) == 0:
280
256
  return
281
257
 
@@ -298,18 +274,14 @@ class StatisticsScreen(TableScreen):
298
274
 
299
275
  def build_dataframe_stats(self) -> None:
300
276
  """Build statistics for the entire dataframe."""
301
- lf = self.df.lazy()
302
-
303
- # Apply only to visible rows
304
- if False in self.dftable.visible_rows:
305
- lf = lf.filter(self.dftable.visible_rows)
277
+ lf = self.df.lazy().select(pl.exclude(RID))
306
278
 
307
279
  # Apply only to non-hidden columns
308
280
  if self.dftable.hidden_columns:
309
281
  lf = lf.select(pl.exclude(self.dftable.hidden_columns))
310
282
 
311
283
  # Get dataframe statistics
312
- stats_df = lf.collect().describe()
284
+ stats_df = lf.describe()
313
285
 
314
286
  # Add columns for each dataframe column with appropriate styling
315
287
  for idx, (col_name, col_dtype) in enumerate(zip(stats_df.columns, stats_df.dtypes), 0):
@@ -348,13 +320,11 @@ class FrequencyScreen(TableScreen):
348
320
  def __init__(self, cidx: int, dftable: "DataFrameTable") -> None:
349
321
  super().__init__(dftable)
350
322
  self.cidx = cidx
351
- self.sorted_columns = {
352
- 1: True, # Count
353
- }
323
+ self.sorted_columns = {1: True} # Count sort by default
324
+ self.total_count = len(dftable.df)
354
325
 
355
- df = dftable.df.filter(dftable.visible_rows) if False in dftable.visible_rows else dftable.df
356
- self.total_count = len(df)
357
- self.df: pl.DataFrame = df[df.columns[self.cidx]].value_counts(sort=True).sort("count", descending=True)
326
+ col = dftable.df.columns[self.cidx]
327
+ self.df: pl.DataFrame = dftable.df.lazy().select(pl.col(col).value_counts(sort=True)).unnest(col).collect()
358
328
 
359
329
  def on_mount(self) -> None:
360
330
  """Create the frequency table."""
@@ -506,7 +476,8 @@ class MetaShape(TableScreen):
506
476
  self.table.add_column(Text("Count", justify="right"))
507
477
 
508
478
  # Get shape information
509
- num_rows, num_cols = self.df.shape
479
+ num_rows, num_cols = self.df.shape if self.dftable.df_view is None else self.dftable.df_view.shape
480
+ num_cols -= 1 # Exclude RID column
510
481
  dc_int = DtypeConfig(pl.Int64)
511
482
 
512
483
  # Add rows to the table
@@ -543,11 +514,14 @@ class MetaColumnScreen(TableScreen):
543
514
 
544
515
  # Add a row for each column
545
516
  for idx, (col_name, col_type) in enumerate(schema.items(), 1):
517
+ if col_name == RID:
518
+ continue # Skip RID column
519
+
546
520
  dc = DtypeConfig(col_type)
547
521
  self.table.add_row(
548
522
  dc_int.format(idx, thousand_separator=self.thousand_separator),
549
523
  col_name,
550
- dc_str.format(col_type, style=dc.style),
524
+ dc_str.format("Datetime" if str(col_type).startswith("Datetime") else col_type, style=dc.style),
551
525
  )
552
526
 
553
527
  self.table.cursor_type = "none"
@@ -119,9 +119,6 @@ class YesNoScreen(ModalScreen):
119
119
  maybe: Optional Maybe button text/dict. Defaults to None.
120
120
  no: Text or dict for the No button. If None, hides the No button. Defaults to "No".
121
121
  on_yes_callback: Optional callable that takes no args and returns the value to dismiss with when Yes is pressed. Defaults to None.
122
-
123
- Returns:
124
- None
125
122
  """
126
123
  super().__init__()
127
124
  self.title = title
@@ -295,32 +292,26 @@ class SaveFileScreen(YesNoScreen):
295
292
 
296
293
  CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "SaveFileScreen")
297
294
 
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)
295
+ def __init__(self, filename: str, save_all: bool = False, tab_count: int = 1):
296
+ self.save_all = save_all
302
297
  super().__init__(
303
- title=title,
304
- label="Enter filename",
298
+ title="Save to File",
299
+ label="Filename",
305
300
  input=filename,
306
- yes="Save",
307
- maybe="Save All Tabs" if self.all_tabs else None,
301
+ yes=f"Save {tab_count} Tabs" if self.save_all else "Save Current Tab" if tab_count > 1 else "Save",
308
302
  no="Cancel",
309
303
  on_yes_callback=self.handle_save,
310
- on_maybe_callback=self.handle_save,
311
304
  )
312
305
 
313
306
  def handle_save(self):
314
307
  if self.input:
315
308
  input_filename = self.input.value.strip()
316
309
  if input_filename:
317
- return input_filename, self.all_tabs, True # Overwrite prompt
310
+ return input_filename, self.save_all, True # Overwrite prompt
318
311
  else:
319
312
  self.notify("Filename cannot be empty", title="Save", severity="error")
320
313
  return None
321
314
 
322
- return None
323
-
324
315
 
325
316
  class ConfirmScreen(YesNoScreen):
326
317
  """Modal screen to ask for confirmation."""
@@ -583,7 +574,7 @@ class EditColumnScreen(YesNoScreen):
583
574
  self.df = df
584
575
  super().__init__(
585
576
  title="Edit Column",
586
- label=f"by value or Polars expression, e.g., abc, pl.lit(7), {NULL}, $_ * 2, $1 + $2, $_.str.to_uppercase(), pl.arange(0, pl.len())",
577
+ label=f"By value or Polars expression, e.g., abc, pl.lit(7), {NULL}, $_ * 2, $1 + $2, $_.str.to_uppercase(), pl.arange(0, pl.len())",
587
578
  input="$_",
588
579
  on_yes_callback=self._get_input,
589
580
  )
@@ -607,8 +598,8 @@ class AddColumnScreen(YesNoScreen):
607
598
  super().__init__(
608
599
  title="Add Column",
609
600
  label="Column name",
610
- input="Link" if link else "Name",
611
- label2="Link template, e.g., https://example.com/$_/id/$1, PC/compound/$cid"
601
+ input="Link" if link else "New column",
602
+ label2="Link template, e.g., https://example.com/$1/id/$_, PC/compound/$cid"
612
603
  if link
613
604
  else "Value or Polars expression, e.g., abc, pl.lit(123), NULL, $_ * 2, $1 + $total, $_ + '_suffix', $_.str.to_uppercase()",
614
605
  input2="Link template" if link else "Value or expression",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataframe-textual
3
- Version: 1.16.2
3
+ Version: 2.0.1
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
@@ -325,7 +325,7 @@ zcat compressed_data.csv.gz | dv -f csv
325
325
 
326
326
  | Key | Action |
327
327
  |-----|--------|
328
- | `\` | Select rows that matches cursor value in current column |
328
+ | `\` | Select rows wth cell matches or those matching cursor value in current column |
329
329
  | `\|` (pipe) | Select rows by expression |
330
330
  | `{` | Go to previous selected row |
331
331
  | `}` | Go to next selected row |
@@ -349,9 +349,9 @@ zcat compressed_data.csv.gz | dv -f csv
349
349
  #### View & Filter
350
350
  | Key | Action |
351
351
  |-----|--------|
352
- | `"` (quote) | Filter to rows that are selected or contain matching cells (and remove others) |
353
- | `v` | View rows (and hide others) by row selections and cell matches or cursor value |
354
- | `V` | View rows (and hide others) by expression |
352
+ | `"` (quote) | Filter selected rows (others removed) |
353
+ | `v` | View selected rows (others hidden) |
354
+ | `V` | View selected by expression (others hidden) |
355
355
 
356
356
  #### SQL Interface
357
357
 
@@ -411,8 +411,8 @@ Press `Enter` on any row to open a modal showing all column values for that row.
411
411
  Useful for examining wide datasets where columns don't fit well on screen.
412
412
 
413
413
  **In the Row Detail Modal**:
414
- - Press `v` to **view** all rows containing the selected column value (and hide others)
415
- - Press `"` to **filter** all rows containing the selected column value (and remove others)
414
+ - Press `v` to **view** all rows containing the selected column value (others hidden but preserved)
415
+ - Press `"` to **filter** all rows containing the selected column value (others removed)
416
416
  - Press `{` to move to the **previous row** (respects hidden rows)
417
417
  - Press `}` to move to the **next row** (respects hidden rows)
418
418
  - Press `q` or `Escape` to close the modal
@@ -421,7 +421,7 @@ Useful for examining wide datasets where columns don't fit well on screen.
421
421
 
422
422
  The application provides multiple modes for selecting rows (marks it for filtering or viewing):
423
423
 
424
- - `\` - Select rows that match cursor value in current column (respects data type)
424
+ - `\` - Select rows with cell matches or those matching cursor value in current column (respects data type)
425
425
  - `|` - Opens dialog to select rows with custom expression
426
426
  - `'` - Select/deselect current row
427
427
  - `t` - Flip selections of all rows
@@ -591,8 +591,8 @@ Press `F` to see value distributions of the current column. The modal shows:
591
591
 
592
592
  **In the Frequency Table**:
593
593
  - Press `[` and `]` to sort by any column (value, count, or percentage)
594
- - Press `v` to **filter** all rows with the selected value (others hidden but preserved)
595
- - Press `"` to **exclude** all rows containing the selected value (others removed)
594
+ - Press `v` to **view** all rows containing the selected value (others hidden but preserved)
595
+ - Press `"` to **filter** all rows containing the selected value (others removed)
596
596
  - Press `q` or `Escape` to close the frequency table
597
597
 
598
598
  This is useful for:
@@ -0,0 +1,14 @@
1
+ dataframe_textual/__init__.py,sha256=E53fW1spQRA4jW9grxSqPEmoe9zofzr6twdveMbt_W8,1310
2
+ dataframe_textual/__main__.py,sha256=vgHjpSsHBF34UN46iMgu_EiebZUzxWwJZ_ngOU3nQvI,3412
3
+ dataframe_textual/common.py,sha256=gpNNY5ZePJkJPf-YoLSCHKaBpdXQ1yW2iSKYV6zZYUo,27908
4
+ dataframe_textual/data_frame_help_panel.py,sha256=UEtj64XsVRdtLzuwOaITfoEQUkAfwFuvpr5Npip5WHs,3381
5
+ dataframe_textual/data_frame_table.py,sha256=p7PXV-39IRimrzdbuDfiacK9yNQ7XXkWBFyZ4finEk8,147693
6
+ dataframe_textual/data_frame_viewer.py,sha256=fkiQ0OGi2rrE06VAVJuAM_9wwmqLY1AZouwEMNoDmy8,22367
7
+ dataframe_textual/sql_screen.py,sha256=P3j1Fv45NIKEYo9adb7NPod54FaU-djFIvCUMMHbvjY,7534
8
+ dataframe_textual/table_screen.py,sha256=XlVxU_haCxPoA41ZIDcwixOg341Wf35JrFwPoCTnMzE,19033
9
+ dataframe_textual/yes_no_screen.py,sha256=NI7Zt3rETDWYiT5CH_FDy7sIWkZ7d7LquaZZbX79b2g,26400
10
+ dataframe_textual-2.0.1.dist-info/METADATA,sha256=XUm9YBbqWaSjAGv8uOrAk8ss1blAjG30BLZwJn2IjwM,29306
11
+ dataframe_textual-2.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ dataframe_textual-2.0.1.dist-info/entry_points.txt,sha256=R_GoooOxcq6ab4RaHiVoZ4zrZJ-phMcGmlL2rwqncW8,107
13
+ dataframe_textual-2.0.1.dist-info/licenses/LICENSE,sha256=AVTg0gk1X-LHI-nnHlAMDQetrwuDZK4eypgSMDO46Yc,1069
14
+ dataframe_textual-2.0.1.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- dataframe_textual/__init__.py,sha256=IFPb8RMUgghw0eRomehkkC684Iny_gs1VkiZMQ5ZpFk,813
2
- dataframe_textual/__main__.py,sha256=IcwIzLxAK26gMCUrwcrukaMzp_PRLzU7AwJ31sYW1Zo,3251
3
- dataframe_textual/common.py,sha256=bV-8WdvAgctLlVWkFIhHLhZi6bIc0QcTo6odYgcB-JU,27735
4
- dataframe_textual/data_frame_help_panel.py,sha256=iEKaur-aH1N_oqHu-vMwEEjfkjQiThK24UO5izsOiW0,3416
5
- dataframe_textual/data_frame_table.py,sha256=NcKqhCYLtFbsly2mj_prqC31r5A4FYcfVHN2TVA5_XI,145800
6
- dataframe_textual/data_frame_viewer.py,sha256=hqieMaogp0WmDvK4KqmvJHYAJXaFF6eATgn3msFkHug,22118
7
- dataframe_textual/sql_screen.py,sha256=_1OZ552s2TV1R0XbsDsfIi1-C3TvggYASomLQoof-Ek,7401
8
- dataframe_textual/table_screen.py,sha256=B0qrAu-rfZahjraMUJEc_kpDYJ0dGi9_xDwlIuTySlM,19742
9
- dataframe_textual/yes_no_screen.py,sha256=HSWmP2rX4Y0hLNfrh3GXRAqsWhSif-jgoB9lo21jfNY,26559
10
- dataframe_textual-1.16.2.dist-info/METADATA,sha256=bB4fNqszkMlyz8MURUgxN9wJ7F0JbhBDg00ojbeBmIQ,29331
11
- dataframe_textual-1.16.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
- dataframe_textual-1.16.2.dist-info/entry_points.txt,sha256=R_GoooOxcq6ab4RaHiVoZ4zrZJ-phMcGmlL2rwqncW8,107
13
- dataframe_textual-1.16.2.dist-info/licenses/LICENSE,sha256=AVTg0gk1X-LHI-nnHlAMDQetrwuDZK4eypgSMDO46Yc,1069
14
- dataframe_textual-1.16.2.dist-info/RECORD,,