dataframe-textual 2.0.0__py3-none-any.whl → 2.1.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.
@@ -20,6 +20,7 @@ from textual.widgets._data_table import (
20
20
  CellDoesNotExist,
21
21
  CellKey,
22
22
  CellType,
23
+ Column,
23
24
  ColumnKey,
24
25
  CursorType,
25
26
  DuplicateKey,
@@ -79,14 +80,13 @@ class History:
79
80
  df: pl.DataFrame
80
81
  df_view: pl.DataFrame | None
81
82
  filename: str
82
- loaded_rows: int
83
83
  hidden_columns: set[str]
84
84
  selected_rows: set[int]
85
85
  sorted_columns: dict[str, bool] # col_name -> descending
86
+ matches: dict[int, set[str]] # RID -> set of col names
86
87
  fixed_rows: int
87
88
  fixed_columns: int
88
89
  cursor_coordinate: Coordinate
89
- matches: dict[int, set[str]] # RID -> set of col names
90
90
  dirty: bool = False # Whether this history state has unsaved changes
91
91
 
92
92
 
@@ -141,7 +141,7 @@ class DataFrameTable(DataTable):
141
141
  - **M** - 📋 Show column metadata (ID, name, type)
142
142
  - **h** - 👁️ Hide current column
143
143
  - **H** - 👀 Show all hidden rows/columns
144
- - **_** - 📏 Expand column to full width
144
+ - **_** - 📏 Toggle column full width
145
145
  - **z** - 📌 Freeze rows and columns
146
146
  - **~** - 🏷️ Toggle row labels
147
147
  - **,** - 🔢 Toggle thousand separator for numeric display
@@ -352,6 +352,9 @@ class DataFrameTable(DataTable):
352
352
  # Whether to use thousand separator for numeric display
353
353
  self.thousand_separator = False
354
354
 
355
+ # Set of columns expanded to full width
356
+ self.expanded_columns: set[str] = set()
357
+
355
358
  # Whether to show internal row index column
356
359
  self.show_rid = False
357
360
 
@@ -1036,7 +1039,7 @@ class DataFrameTable(DataTable):
1036
1039
  Returns:
1037
1040
  dict[str, int]: Mapping of column name to width (None for auto-sizing columns).
1038
1041
  """
1039
- column_widths = {}
1042
+ col_widths, col_label_widths = {}, {}
1040
1043
 
1041
1044
  # Get available width for the table (with some padding for borders/scrollbar)
1042
1045
  available_width = self.scrollable_content_region.width
@@ -1046,7 +1049,7 @@ class DataFrameTable(DataTable):
1046
1049
 
1047
1050
  # No string columns, let TextualDataTable auto-size all columns
1048
1051
  if not string_cols:
1049
- return column_widths
1052
+ return col_widths
1050
1053
 
1051
1054
  # Sample a reasonable number of rows to calculate widths (don't scan entire dataframe)
1052
1055
  sample_size = min(self.BATCH_SIZE, len(self.df))
@@ -1060,7 +1063,10 @@ class DataFrameTable(DataTable):
1060
1063
  # Get column label width
1061
1064
  # Add padding for sort indicators if any
1062
1065
  label_width = measure(self.app.console, col, 1) + 2
1063
- if dtype != pl.String:
1066
+ col_label_widths[col] = label_width
1067
+
1068
+ # Let Textual auto-size for non-string columns and already expanded columns
1069
+ if dtype != pl.String or col in self.expanded_columns:
1064
1070
  available_width -= label_width
1065
1071
  continue
1066
1072
 
@@ -1083,16 +1089,16 @@ class DataFrameTable(DataTable):
1083
1089
  max_width = label_width
1084
1090
  self.log(f"Error determining width for column '{col}': {e}")
1085
1091
 
1086
- column_widths[col] = max_width
1092
+ col_widths[col] = max_width
1087
1093
  available_width -= max_width
1088
1094
 
1089
1095
  # If there's no more available width, auto-size remaining columns
1090
1096
  if available_width < 0:
1091
- for col in column_widths:
1092
- if column_widths[col] > STRING_WIDTH_CAP:
1093
- column_widths[col] = STRING_WIDTH_CAP # Cap string columns
1097
+ for col in col_widths:
1098
+ if col_widths[col] > STRING_WIDTH_CAP and col_label_widths[col] < STRING_WIDTH_CAP:
1099
+ col_widths[col] = STRING_WIDTH_CAP # Cap string columns
1094
1100
 
1095
- return column_widths
1101
+ return col_widths
1096
1102
 
1097
1103
  def setup_columns(self) -> None:
1098
1104
  """Clear table and setup columns.
@@ -1487,6 +1493,9 @@ class DataFrameTable(DataTable):
1487
1493
  if start % self.BATCH_SIZE != 0:
1488
1494
  start = (start // self.BATCH_SIZE + 1) * self.BATCH_SIZE
1489
1495
 
1496
+ if stop - start < self.BATCH_SIZE:
1497
+ start -= self.BATCH_SIZE
1498
+
1490
1499
  self.load_rows_range(start, stop)
1491
1500
  self.move_cursor(row=self.row_count - 1)
1492
1501
 
@@ -1519,14 +1528,13 @@ class DataFrameTable(DataTable):
1519
1528
  df=self.df,
1520
1529
  df_view=self.df_view,
1521
1530
  filename=self.filename,
1522
- loaded_rows=self.loaded_rows,
1523
1531
  hidden_columns=self.hidden_columns.copy(),
1524
1532
  selected_rows=self.selected_rows.copy(),
1525
1533
  sorted_columns=self.sorted_columns.copy(),
1534
+ matches={k: v.copy() for k, v in self.matches.items()},
1526
1535
  fixed_rows=self.fixed_rows,
1527
1536
  fixed_columns=self.fixed_columns,
1528
1537
  cursor_coordinate=self.cursor_coordinate,
1529
- matches={k: v.copy() for k, v in self.matches.items()},
1530
1538
  dirty=self.dirty,
1531
1539
  )
1532
1540
 
@@ -1539,14 +1547,13 @@ class DataFrameTable(DataTable):
1539
1547
  self.df = history.df
1540
1548
  self.df_view = history.df_view
1541
1549
  self.filename = history.filename
1542
- self.loaded_rows = history.loaded_rows
1543
1550
  self.hidden_columns = history.hidden_columns.copy()
1544
1551
  self.selected_rows = history.selected_rows.copy()
1545
1552
  self.sorted_columns = history.sorted_columns.copy()
1553
+ self.matches = {k: v.copy() for k, v in history.matches.items()} if history.matches else defaultdict(set)
1546
1554
  self.fixed_rows = history.fixed_rows
1547
1555
  self.fixed_columns = history.fixed_columns
1548
1556
  self.cursor_coordinate = history.cursor_coordinate
1549
- self.matches = {k: v.copy() for k, v in history.matches.items()} if history.matches else defaultdict(set)
1550
1557
  self.dirty = history.dirty
1551
1558
 
1552
1559
  # Recreate table for display
@@ -1710,11 +1717,15 @@ class DataFrameTable(DataTable):
1710
1717
  if dtype != pl.String:
1711
1718
  return
1712
1719
 
1720
+ # The column to expand/shrink
1721
+ col: Column = self.columns[col_key]
1722
+
1713
1723
  # Calculate the maximum width across all loaded rows
1714
- max_width = len(col_name) + 2 # Start with column name width + padding
1724
+ label_width = len(col_name) + 2 # Start with column name width + padding
1715
1725
 
1716
1726
  try:
1717
1727
  need_expand = False
1728
+ max_width = label_width
1718
1729
 
1719
1730
  # Scan through all loaded rows that are visible to find max width
1720
1731
  for row_idx in range(self.loaded_rows):
@@ -1728,22 +1739,28 @@ class DataFrameTable(DataTable):
1728
1739
  if not need_expand:
1729
1740
  return
1730
1741
 
1731
- # Update the column width
1732
- col = self.columns[col_key]
1733
- col.width = max_width
1742
+ if col_name in self.expanded_columns:
1743
+ col.width = max(label_width, STRING_WIDTH_CAP)
1744
+ self.expanded_columns.remove(col_name)
1745
+ else:
1746
+ self.expanded_columns.add(col_name)
1734
1747
 
1735
- # Force a refresh
1736
- self._update_count += 1
1737
- self._require_update_dimensions = True
1738
- self.refresh(layout=True)
1748
+ # Update the column width
1749
+ col.width = max_width
1739
1750
 
1740
- # self.notify(f"Expanded column [$success]{col_name}[/] to width [$accent]{max_width}[/]", title="Expand")
1741
1751
  except Exception as e:
1742
1752
  self.notify(
1743
1753
  f"Error expanding column [$error]{col_name}[/]", title="Expand Column", severity="error", timeout=10
1744
1754
  )
1745
1755
  self.log(f"Error expanding column `{col_name}`: {str(e)}")
1746
1756
 
1757
+ # Force a refresh
1758
+ self._update_count += 1
1759
+ self._require_update_dimensions = True
1760
+ self.refresh(layout=True)
1761
+
1762
+ # self.notify(f"Expanded column [$success]{col_name}[/] to width [$accent]{max_width}[/]", title="Expand")
1763
+
1747
1764
  def do_toggle_rid(self) -> None:
1748
1765
  """Toggle display of the internal RID column."""
1749
1766
  self.show_rid = not self.show_rid
@@ -1965,13 +1982,21 @@ class DataFrameTable(DataTable):
1965
1982
  if self.df_view is not None:
1966
1983
  # Get updated column from df for rows that exist in df_view
1967
1984
  col_updated = f"^_{col_name}_^"
1968
- lf_updated = self.df.lazy().select(RID, pl.col(col_name).alias(col_updated))
1969
- # Join and use coalesce to prefer updated value or keep original
1985
+ col_exists = "^_exists_^"
1986
+ lf_updated = self.df.lazy().select(
1987
+ RID, pl.col(col_name).alias(col_updated), pl.lit(True).alias(col_exists)
1988
+ )
1989
+ # Join and use when/then/otherwise to handle all updates including NULLs
1970
1990
  self.df_view = (
1971
1991
  self.df_view.lazy()
1972
1992
  .join(lf_updated, on=RID, how="left")
1973
- .with_columns(pl.coalesce(pl.col(col_updated), pl.col(col_name)).alias(col_name))
1974
- .drop(col_updated)
1993
+ .with_columns(
1994
+ pl.when(pl.col(col_exists))
1995
+ .then(pl.col(col_updated))
1996
+ .otherwise(pl.col(col_name))
1997
+ .alias(col_name)
1998
+ )
1999
+ .drop(col_updated, col_exists)
1975
2000
  .collect()
1976
2001
  )
1977
2002
  except Exception as e:
@@ -2269,7 +2294,12 @@ class DataFrameTable(DataTable):
2269
2294
  """Remove the currently selected column from the table."""
2270
2295
  # Get the column to remove
2271
2296
  col_idx = self.cursor_column
2272
- col_name = self.cursor_col_name
2297
+ try:
2298
+ col_name = self.cursor_col_name
2299
+ except CellDoesNotExist:
2300
+ self.notify("No column to delete at the current cursor position", title="Delete Column", severity="warning")
2301
+ return
2302
+
2273
2303
  col_key = self.cursor_col_key
2274
2304
 
2275
2305
  col_names_to_remove = []
@@ -2334,7 +2364,7 @@ class DataFrameTable(DataTable):
2334
2364
  if self.df_view is not None:
2335
2365
  self.df_view = self.df_view.drop(col_names_to_remove)
2336
2366
 
2337
- self.notify(message, title="Delete")
2367
+ self.notify(message, title="Delete Column")
2338
2368
 
2339
2369
  def do_duplicate_column(self) -> None:
2340
2370
  """Duplicate the currently selected column, inserting it right after the current column."""
@@ -2386,7 +2416,7 @@ class DataFrameTable(DataTable):
2386
2416
  # Delete all selected rows
2387
2417
  if selected_count := len(self.selected_rows):
2388
2418
  history_desc = f"Deleted {selected_count} selected row(s)"
2389
- rids_to_delete = self.selected_rows
2419
+ rids_to_delete.update(self.selected_rows)
2390
2420
 
2391
2421
  # Delete current row and those above
2392
2422
  elif more == "above":
@@ -549,7 +549,7 @@ class DataFrameViewer(App):
549
549
  )
550
550
  self.push_screen(
551
551
  ConfirmScreen(
552
- "Close All Tabs",
552
+ "Close All Tabs" if len(self.tabs) > 1 else "Close Tab",
553
553
  label=label,
554
554
  yes="Save",
555
555
  maybe="Discard",
@@ -163,7 +163,7 @@ class RowDetailScreen(TableScreen):
163
163
 
164
164
  # Get all columns and values from the dataframe row
165
165
  for col, val, dtype in zip(self.df.columns, self.df.row(self.ridx), self.df.dtypes):
166
- if col == RID:
166
+ if col in self.dftable.hidden_columns or col == RID:
167
167
  continue # Skip RID column
168
168
  formatted_row = []
169
169
  formatted_row.append(col)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataframe-textual
3
- Version: 2.0.0
3
+ Version: 2.1.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
@@ -247,8 +247,8 @@ zcat compressed_data.csv.gz | dv -f csv
247
247
  | `Q` | Close all tabs and app (prompts to save unsaved changes) |
248
248
  | `Ctrl+Q` | Force to quit app (regardless of unsaved changes) |
249
249
  | `Ctrl+T` | Save current tab to file |
250
+ | `Ctrl+A` | Save all tabs to a Excel file |
250
251
  | `w` | Save current tab to file (overwrite without prompt) |
251
- | `Ctrl+A` | Save all tabs to file |
252
252
  | `W` | Save all tabs to file (overwrite without prompt) |
253
253
  | `Ctrl+D` | Duplicate current tab |
254
254
  | `Ctrl+O` | Open file in a new tab |
@@ -271,7 +271,7 @@ zcat compressed_data.csv.gz | dv -f csv
271
271
  | Key | Action |
272
272
  |-----|--------|
273
273
  | `g` | Jump to first row |
274
- | `G` | Jump to last row (loads all remaining rows) |
274
+ | `G` | Jump to last row |
275
275
  | `↑` / `↓` | Move up/down one row |
276
276
  | `←` / `→` | Move left/right one column |
277
277
  | `Home` / `End` | Jump to first/last column |
@@ -295,9 +295,11 @@ zcat compressed_data.csv.gz | dv -f csv
295
295
  | `F` | Show frequency distribution for current column |
296
296
  | `s` | Show statistics for current column |
297
297
  | `S` | Show statistics for entire dataframe |
298
+ | `m` | Show metadata for row count and column count |
299
+ | `M` | Show metadata for current column |
298
300
  | `K` | Cycle cursor types: cell → row → column → cell |
299
301
  | `~` | Toggle row labels |
300
- | `_` (underscore) | Expand column to full width |
302
+ | `_` (underscore) | Toggle column full width |
301
303
  | `z` | Freeze rows and columns |
302
304
  | `,` | Toggle thousand separator for numeric display |
303
305
  | `h` | Hide current column |
@@ -318,7 +320,7 @@ zcat compressed_data.csv.gz | dv -f csv
318
320
  | `x` | Delete current row |
319
321
  | `X` | Delete current row and all those below |
320
322
  | `Ctrl+X` | Delete current row and all those above |
321
- | `d` | Duplicate current column (appends '_copy' suffix) |
323
+ | `d` | Duplicate current column |
322
324
  | `D` | Duplicate current row |
323
325
 
324
326
  #### Row Selection
@@ -392,7 +394,7 @@ zcat compressed_data.csv.gz | dv -f csv
392
394
  | `c` | Copy current cell to clipboard |
393
395
  | `Ctrl+C` | Copy column to clipboard |
394
396
  | `Ctrl+R` | Copy row to clipboard (tab-separated) |
395
- | `Ctrl+S` | Save current tab to file |
397
+ | `Ctrl+S` | Save to file |
396
398
 
397
399
  ## Features in Detail
398
400
 
@@ -413,8 +415,8 @@ Useful for examining wide datasets where columns don't fit well on screen.
413
415
  **In the Row Detail Modal**:
414
416
  - Press `v` to **view** all rows containing the selected column value (others hidden but preserved)
415
417
  - Press `"` to **filter** all rows containing the selected column value (others removed)
416
- - Press `{` to move to the **previous row** (respects hidden rows)
417
- - Press `}` to move to the **next row** (respects hidden rows)
418
+ - Press `{` to move to the previous row
419
+ - Press `}` to move to the next row
418
420
  - Press `q` or `Escape` to close the modal
419
421
 
420
422
  ### 3. Row Selection
@@ -429,7 +431,7 @@ The application provides multiple modes for selecting rows (marks it for filteri
429
431
  - `{` - Go to previous selected row
430
432
  - `}` - Go to next selected row
431
433
 
432
- **Advanced Matching Options**:
434
+ **Advanced Options**:
433
435
 
434
436
  When searching or finding, you can use checkboxes in the dialog to enable:
435
437
  - **Match Nocase**: Ignore case differences
@@ -445,7 +447,7 @@ These options work with plain text searches. Use Polars regex patterns in expres
445
447
  - Use `u` to undo any search or filter
446
448
 
447
449
  ### 4. Find & Replace
448
- **Find Operations** - Find by value/expression and highlight matching cells:
450
+ Find by value/expression and highlight matching cells:
449
451
  - `/` - Find cursor value within current column (respects data type)
450
452
  - `?` - Open dialog to search current column with expression
451
453
  - `;` - Find cursor value across all columns
@@ -469,10 +471,10 @@ When you press `r` or `R`, enter:
469
471
 
470
472
  **Replace Interactive**:
471
473
  - Review each match one at a time (confirm, skip, or cancel)
472
- - Shows progress: `X of Y`
474
+ - Shows progress
473
475
 
474
476
  **Tips:**
475
- - Search are done by string value (i.e. ignoring data type)
477
+ - Search are done by string value (i.e., ignoring data type)
476
478
  - Type `NULL` to replace null/missing values
477
479
  - Use `Match Nocase` for case-insensitive matching
478
480
  - Use `Match Whole` to avoid partial replacements
@@ -480,7 +482,7 @@ When you press `r` or `R`, enter:
480
482
 
481
483
  ### 5. Filter vs. View
482
484
 
483
- Both operations show rows that are selected or contain matching cells, but with fundamentally different effects:
485
+ Both operations show selected rows but with fundamentally different effects:
484
486
 
485
487
  | Operation | Keyboard | Effect | Data Preserved |
486
488
  |-----------|----------|--------|-----------------|
@@ -571,8 +573,8 @@ View quick metadata about your dataframe and columns to understand their structu
571
573
 
572
574
  **Dataframe Metadata** (`m`):
573
575
  - Press `m` to open a modal displaying:
574
- - **Rows** - Total number of rows in the dataframe
575
- - **Columns** - Total number of columns in the dataframe
576
+ - **Row** - Total number of rows in the dataframe
577
+ - **Column** - Total number of columns in the dataframe
576
578
 
577
579
  **Column Metadata** (`M`):
578
580
  - Press `M` to open a modal displaying details for all columns:
@@ -751,7 +753,6 @@ SELECT specific columns and apply WHERE conditions without writing full SQL:
751
753
  #### Advanced SQL Interface (`L`)
752
754
  Execute complete SQL queries for advanced data manipulation:
753
755
  - Write full SQL queries with standard [SQL syntax](https://docs.pola.rs/api/python/stable/reference/sql/index.html)
754
- - Support for JOINs, GROUP BY, aggregations, and more
755
756
  - Access to all SQL capabilities for complex transformations
756
757
  - Always use `self` as the table name
757
758
  - Syntax highlighted
@@ -773,7 +774,7 @@ WHERE `product id` = 7
773
774
 
774
775
  Copies value to system clipboard with `pbcopy` on macOS and `xclip` on Linux.
775
776
 
776
- **Note** May require a X server to work.
777
+ **Note**: may require a X server to work.
777
778
 
778
779
  - Press `c` to copy cursor value
779
780
  - Press `Ctrl+C` to copy column values
@@ -815,8 +816,8 @@ Manage multiple files and dataframes simultaneously with tabs.
815
816
  - **`Double-click`** - Rename the tab
816
817
  - **`Ctrl+D`** - Duplicate current tab (creates a copy with same data and state)
817
818
  - **`Ctrl+T`** - Save current tab to file
818
- - **`w`** - Save current tab to file (overwrite without prompt)
819
819
  - **`Ctrl+A`** - Save all tabs in a single Excel file
820
+ - **`w`** - Save current tab to file (overwrite without prompt)
820
821
  - **`W`** - Save all tabs to file (overwrite without prompt)
821
822
  - **`q`** - Close current tab (closes tab, prompts to save if unsaved changes)
822
823
  - **`Q`** - Close all tabs and exit app (prompts to save tabs with unsaved changes)
@@ -2,13 +2,13 @@ dataframe_textual/__init__.py,sha256=E53fW1spQRA4jW9grxSqPEmoe9zofzr6twdveMbt_W8
2
2
  dataframe_textual/__main__.py,sha256=vgHjpSsHBF34UN46iMgu_EiebZUzxWwJZ_ngOU3nQvI,3412
3
3
  dataframe_textual/common.py,sha256=gpNNY5ZePJkJPf-YoLSCHKaBpdXQ1yW2iSKYV6zZYUo,27908
4
4
  dataframe_textual/data_frame_help_panel.py,sha256=UEtj64XsVRdtLzuwOaITfoEQUkAfwFuvpr5Npip5WHs,3381
5
- dataframe_textual/data_frame_table.py,sha256=4tnE6apxQ_EJoSvFwq4ZuDqKTHSBUcQVFYSDbweIK98,147612
6
- dataframe_textual/data_frame_viewer.py,sha256=fkiQ0OGi2rrE06VAVJuAM_9wwmqLY1AZouwEMNoDmy8,22367
5
+ dataframe_textual/data_frame_table.py,sha256=pPddPOa3hV3voCf3_9botg0jeNcPdWsN7Oiw5vKdMdU,148701
6
+ dataframe_textual/data_frame_viewer.py,sha256=pX1xU3REQnMMEHapYKmr_FH1hFGExqTrOjjFWBx7dtg,22406
7
7
  dataframe_textual/sql_screen.py,sha256=P3j1Fv45NIKEYo9adb7NPod54FaU-djFIvCUMMHbvjY,7534
8
- dataframe_textual/table_screen.py,sha256=XlVxU_haCxPoA41ZIDcwixOg341Wf35JrFwPoCTnMzE,19033
8
+ dataframe_textual/table_screen.py,sha256=XPzJI6FXjwnxtQSMTmluygwkYM-0-Lx3v9o-MuL6bMg,19071
9
9
  dataframe_textual/yes_no_screen.py,sha256=NI7Zt3rETDWYiT5CH_FDy7sIWkZ7d7LquaZZbX79b2g,26400
10
- dataframe_textual-2.0.0.dist-info/METADATA,sha256=9prx6qymZRsp42WCJbl1S9qluBkY2IBRDMN-JHiBL_U,29306
11
- dataframe_textual-2.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
- dataframe_textual-2.0.0.dist-info/entry_points.txt,sha256=R_GoooOxcq6ab4RaHiVoZ4zrZJ-phMcGmlL2rwqncW8,107
13
- dataframe_textual-2.0.0.dist-info/licenses/LICENSE,sha256=AVTg0gk1X-LHI-nnHlAMDQetrwuDZK4eypgSMDO46Yc,1069
14
- dataframe_textual-2.0.0.dist-info/RECORD,,
10
+ dataframe_textual-2.1.0.dist-info/METADATA,sha256=6RZnWlkm6mWKnQNMUTUGBeklOFYXQgWtd1lOJmz5oX0,29160
11
+ dataframe_textual-2.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ dataframe_textual-2.1.0.dist-info/entry_points.txt,sha256=R_GoooOxcq6ab4RaHiVoZ4zrZJ-phMcGmlL2rwqncW8,107
13
+ dataframe_textual-2.1.0.dist-info/licenses/LICENSE,sha256=AVTg0gk1X-LHI-nnHlAMDQetrwuDZK4eypgSMDO46Yc,1069
14
+ dataframe_textual-2.1.0.dist-info/RECORD,,