dataframe-textual 1.9.0__py3-none-any.whl → 1.10.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.
@@ -916,6 +916,27 @@ class DataFrameTable(DataTable):
916
916
  self.check_and_load_more()
917
917
 
918
918
  # Setup & Loading
919
+ def reset_df(self, new_df: pl.DataFrame, dirty: bool = True) -> None:
920
+ """Reset the dataframe to a new one and refresh the table.
921
+
922
+ Args:
923
+ new_df: The new Polars DataFrame to set.
924
+ dirty: Whether to mark the table as dirty (unsaved changes). Defaults to True.
925
+ """
926
+ # Set new dataframe and reset table
927
+ self.df = new_df
928
+ self.loaded_rows = 0
929
+ self.sorted_columns = {}
930
+ self.hidden_columns = set()
931
+ self.selected_rows = [False] * len(self.df)
932
+ self.visible_rows = [True] * len(self.df)
933
+ self.fixed_rows = 0
934
+ self.fixed_columns = 0
935
+ self.matches = defaultdict(set)
936
+ self.histories.clear()
937
+ self.history = None
938
+ self.dirty = dirty # Mark as dirty since data changed
939
+
919
940
  def setup_table(self, reset: bool = False) -> None:
920
941
  """Setup the table for display.
921
942
 
@@ -927,18 +948,7 @@ class DataFrameTable(DataTable):
927
948
 
928
949
  # Reset to original dataframe
929
950
  if reset:
930
- self.df = self.dataframe
931
- self.loaded_rows = 0
932
- self.sorted_columns = {}
933
- self.hidden_columns = set()
934
- self.selected_rows = [False] * len(self.df)
935
- self.visible_rows = [True] * len(self.df)
936
- self.fixed_rows = 0
937
- self.fixed_columns = 0
938
- self.matches = defaultdict(set)
939
- self.histories.clear()
940
- self.history = None
941
- self.dirty = False
951
+ self.reset_df(self.dataframe, dirty=False)
942
952
 
943
953
  # Lazy load up to INITIAL_BATCH_SIZE visible rows
944
954
  stop, visible_count = self.INITIAL_BATCH_SIZE, 0
@@ -3010,20 +3020,15 @@ class DataFrameTable(DataTable):
3010
3020
  # Apply filter to dataframe with row indices
3011
3021
  df_filtered = self.df.with_row_index(RIDX).filter(filter_expr)
3012
3022
 
3013
- # Update selections and matches
3014
- self.selected_rows = [self.selected_rows[ridx] for ridx in df_filtered[RIDX]]
3015
- self.matches = {
3016
- idx: self.matches[ridx].copy() for idx, ridx in enumerate(df_filtered[RIDX]) if ridx in self.matches
3017
- }
3018
-
3019
3023
  # Update dataframe
3020
- self.df = df_filtered.drop(RIDX)
3024
+ self.reset_df(df_filtered.drop(RIDX))
3021
3025
 
3022
3026
  # Recreate table for display
3023
3027
  self.setup_table()
3024
3028
 
3025
3029
  self.notify(
3026
- f"Removed rows without selections or matches. Now showing [$accent]{len(self.df)}[/] rows", title="Filter"
3030
+ f"Filtered rows with selections or matches and removed others. Now showing [$accent]{len(self.df)}[/] rows",
3031
+ title="Filter",
3027
3032
  )
3028
3033
 
3029
3034
  def do_view_rows(self) -> None:
@@ -3043,7 +3048,8 @@ class DataFrameTable(DataTable):
3043
3048
  # Otherwise, use the current cell value
3044
3049
  else:
3045
3050
  ridx = self.cursor_row_idx
3046
- term = str(self.df.item(ridx, cidx))
3051
+ value = self.df.item(ridx, cidx)
3052
+ term = NULL if value is None else str(value)
3047
3053
 
3048
3054
  self.view_rows((term, cidx, False, True))
3049
3055
 
@@ -3051,10 +3057,11 @@ class DataFrameTable(DataTable):
3051
3057
  """Open the filter screen to enter an expression."""
3052
3058
  ridx = self.cursor_row_idx
3053
3059
  cidx = self.cursor_col_idx
3054
- cursor_value = str(self.df.item(ridx, cidx))
3060
+ cursor_value = self.df.item(ridx, cidx)
3061
+ term = NULL if cursor_value is None else str(cursor_value)
3055
3062
 
3056
3063
  self.app.push_screen(
3057
- FilterScreen(self.df, cidx, cursor_value),
3064
+ FilterScreen(self.df, cidx, term),
3058
3065
  callback=self.view_rows,
3059
3066
  )
3060
3067
 
@@ -3108,10 +3115,7 @@ class DataFrameTable(DataTable):
3108
3115
  if False in self.visible_rows:
3109
3116
  lf = lf.filter(self.visible_rows)
3110
3117
 
3111
- if isinstance(expr, (list, pl.Series)):
3112
- expr_str = str(list(expr)[:10]) + ("..." if len(expr) > 10 else "")
3113
- else:
3114
- expr_str = str(expr)
3118
+ expr_str = "boolean list or series" if isinstance(expr, (list, pl.Series)) else str(expr)
3115
3119
 
3116
3120
  # Apply the filter expression
3117
3121
  try:
@@ -3128,7 +3132,7 @@ class DataFrameTable(DataTable):
3128
3132
  return
3129
3133
 
3130
3134
  # Add to history
3131
- self.add_history(f"Filtered by expression [$success]{expr_str}[/]", dirty=True)
3135
+ self.add_history(f"Filtered by expression [$success]{expr_str}[/]")
3132
3136
 
3133
3137
  # Mark unfiltered rows as invisible
3134
3138
  filtered_row_indices = set(df_filtered[RIDX].to_list())
@@ -81,10 +81,6 @@ class DataFrameViewer(App):
81
81
  ContentTab.dirty {
82
82
  background: $warning-darken-3;
83
83
  }
84
-
85
- .underline--bar {
86
- color: red;
87
- }
88
84
  """
89
85
 
90
86
  def __init__(self, *sources: Source) -> None:
@@ -157,11 +157,8 @@ class SimpleSqlScreen(SqlScreen):
157
157
  """
158
158
  super().__init__(
159
159
  dftable,
160
- on_yes_callback=self._handle_simple,
161
- on_maybe_callback=partial(
162
- self._handle_simple,
163
- view=False,
164
- ),
160
+ on_yes_callback=self.handle_simple,
161
+ on_maybe_callback=partial(self.handle_simple, view=False),
165
162
  )
166
163
 
167
164
  def compose(self) -> ComposeResult:
@@ -177,7 +174,7 @@ class SimpleSqlScreen(SqlScreen):
177
174
  yield Input(placeholder="e.g., age > 30 and height < 180", id="where-input")
178
175
  yield from super().compose()
179
176
 
180
- def _handle_simple(self, view: bool = True) -> None:
177
+ def handle_simple(self, view: bool = True) -> None:
181
178
  """Handle Yes button/Enter key press."""
182
179
  selections = self.query_one(SelectionList).selected
183
180
  if not selections:
@@ -221,8 +218,8 @@ class AdvancedSqlScreen(SqlScreen):
221
218
  """
222
219
  super().__init__(
223
220
  dftable,
224
- on_yes_callback=self._handle_advanced,
225
- on_maybe_callback=partial(self._handle_advanced, view=False),
221
+ on_yes_callback=self.handle_advanced,
222
+ on_maybe_callback=partial(self.handle_advanced, view=False),
226
223
  )
227
224
 
228
225
  def compose(self) -> ComposeResult:
@@ -236,6 +233,6 @@ class AdvancedSqlScreen(SqlScreen):
236
233
  )
237
234
  yield from super().compose()
238
235
 
239
- def _handle_advanced(self, view: bool = True) -> None:
236
+ def handle_advanced(self, view: bool = True) -> None:
240
237
  """Handle Yes button/Enter key press."""
241
238
  return self.query_one(TextArea).text.strip(), view
@@ -97,60 +97,61 @@ class TableScreen(ModalScreen):
97
97
  self.build_table()
98
98
  event.stop()
99
99
 
100
- def _filter_or_highlight_selected_value(
101
- self, col_name_value: tuple[str, Any] | None, action: str = "filter"
102
- ) -> None:
103
- """Apply filter or highlight action by the selected value.
100
+ def filter_or_view_selected_value(self, cidx_name_value: tuple[int, str, Any] | None, action: str = "view") -> None:
101
+ """Apply filter or view action by the selected value.
104
102
 
105
- Filters or highlights rows in the main table based on a selected value from
103
+ Filters or views rows in the main table based on a selected value from
106
104
  this table (typically frequency or row detail). Updates the main table's display
107
105
  and notifies the user of the action.
108
106
 
109
107
  Args:
110
- col_name_value: Tuple of (column_name, column_value) to filter/highlight by, or None.
111
- action: Either "filter" to hide non-matching rows, or "highlight" to select matching rows. Defaults to "filter".
112
-
113
- Returns:
114
- None
108
+ col_name_value: Tuple of (column_name, column_value) to filter/view by, or None.
109
+ action: Either "filter" to hide non-matching rows, or "view" to show matching rows. Defaults to "view".
115
110
  """
116
- if col_name_value is None:
111
+ if cidx_name_value is None:
117
112
  return
118
- col_name, col_value = col_name_value
113
+ cidx, col_name, col_value = cidx_name_value
114
+ self.log(f"Filtering or viewing by {col_name} == {col_value}")
119
115
 
120
116
  # Handle NULL values
121
117
  if col_value == NULL:
122
118
  # Create expression for NULL values
123
119
  expr = pl.col(col_name).is_null()
124
- value_display = "[$success]NULL[/]"
120
+ value_display = f"[$success]{NULL_DISPLAY}[/]"
125
121
  else:
126
122
  # Create expression for the selected value
127
123
  expr = pl.col(col_name) == col_value
128
124
  value_display = f"[$success]{col_value}[/]"
129
125
 
130
- matched_indices = set(self.dftable.df.with_row_index(RIDX).filter(expr)[RIDX].to_list())
126
+ df_filtered = self.dftable.df.with_row_index(RIDX).filter(expr)
127
+ self.log(f"Filtered dataframe has {len(df_filtered)} rows")
128
+
129
+ matched_indices = set(df_filtered[RIDX].to_list())
130
+ if not matched_indices:
131
+ self.notify(
132
+ f"No matches found for [$warning]{col_name}[/] == {value_display}",
133
+ title="No Matches",
134
+ severity="warning",
135
+ )
136
+ return
131
137
 
132
138
  # Apply the action
133
139
  if action == "filter":
134
- # Update visible_rows to reflect the filter
135
- for i in range(len(self.dftable.visible_rows)):
136
- self.dftable.visible_rows[i] = i in matched_indices
137
- title = "Filter"
138
- message = f"Filtered by [$accent]{col_name}[/] == [$success]{value_display}[/]"
139
- else: # action == "highlight"
140
- # Update selected_rows to reflect the highlights
140
+ # Update selections
141
141
  for i in range(len(self.dftable.selected_rows)):
142
142
  self.dftable.selected_rows[i] = i in matched_indices
143
- title = "Highlight"
144
- message = f"Highlighted [$accent]{col_name}[/] == [$success]{value_display}[/]"
145
143
 
146
- # Recreate the table display with updated data in the main app
147
- self.dftable.setup_table()
144
+ # Update main table display
145
+ self.dftable.do_filter_rows()
146
+
147
+ else: # action == "view"
148
+ # Update visible rows
149
+ expr = [i in matched_indices for i in range(len(self.dftable.df))]
150
+ self.dftable.view_rows((expr, cidx, False, True))
148
151
 
149
152
  # Dismiss the frequency screen
150
153
  self.app.pop_screen()
151
154
 
152
- self.notify(message, title=title)
153
-
154
155
 
155
156
  class RowDetailScreen(TableScreen):
156
157
  """Modal screen to display a single row's details."""
@@ -199,30 +200,27 @@ class RowDetailScreen(TableScreen):
199
200
 
200
201
  Args:
201
202
  event: The key event object.
202
-
203
- Returns:
204
- None
205
203
  """
206
204
  if event.key == "v":
207
- # Filter the main table by the selected value
208
- self._filter_or_highlight_selected_value(self._get_col_name_value(), action="filter")
205
+ # View the main table by the selected value
206
+ self.filter_or_view_selected_value(self.get_cidx_name_value(), action="view")
209
207
  event.stop()
210
208
  elif event.key == "quotation_mark": # '"'
211
- # Highlight the main table by the selected value
212
- self._filter_or_highlight_selected_value(self._get_col_name_value(), action="highlight")
209
+ # Filter the main table by the selected value
210
+ self.filter_or_view_selected_value(self.get_cidx_name_value(), action="filter")
213
211
  event.stop()
214
212
  elif event.key == "comma":
215
213
  event.stop()
216
214
 
217
- def _get_col_name_value(self) -> tuple[str, Any] | None:
218
- row_idx = self.table.cursor_row
219
- if row_idx >= len(self.df.columns):
215
+ def get_cidx_name_value(self) -> tuple[int, str, Any] | None:
216
+ cidx = self.table.cursor_row
217
+ if cidx >= len(self.df.columns):
220
218
  return None # Invalid row
221
219
 
222
- col_name = self.df.columns[row_idx]
223
- col_value = self.df.item(self.ridx, row_idx)
220
+ col_name = self.df.columns[cidx]
221
+ col_value = self.df.item(self.ridx, cidx)
224
222
 
225
- return col_name, col_value
223
+ return cidx, col_name, col_value
226
224
 
227
225
 
228
226
  class StatisticsScreen(TableScreen):
@@ -244,14 +242,14 @@ class StatisticsScreen(TableScreen):
244
242
 
245
243
  if self.col_idx is None:
246
244
  # Dataframe statistics
247
- self._build_dataframe_stats()
245
+ self.build_dataframe_stats()
248
246
  self.table.cursor_type = "column"
249
247
  else:
250
248
  # Column statistics
251
- self._build_column_stats()
249
+ self.build_column_stats()
252
250
  self.table.cursor_type = "row"
253
251
 
254
- def _build_column_stats(self) -> None:
252
+ def build_column_stats(self) -> None:
255
253
  """Build statistics for a single column."""
256
254
  col_name = self.df.columns[self.col_idx]
257
255
  lf = self.df.lazy()
@@ -292,7 +290,7 @@ class StatisticsScreen(TableScreen):
292
290
  Text(value, style=dc.style, justify=dc.justify),
293
291
  )
294
292
 
295
- def _build_dataframe_stats(self) -> None:
293
+ def build_dataframe_stats(self) -> None:
296
294
  """Build statistics for the entire dataframe."""
297
295
  lf = self.df.lazy()
298
296
 
@@ -351,16 +349,16 @@ class FrequencyScreen(TableScreen):
351
349
 
352
350
  CSS = TableScreen.DEFAULT_CSS.replace("TableScreen", "FrequencyScreen")
353
351
 
354
- def __init__(self, col_idx: int, dftable: "DataFrameTable") -> None:
352
+ def __init__(self, cidx: int, dftable: "DataFrameTable") -> None:
355
353
  super().__init__(dftable)
356
- self.col_idx = col_idx
354
+ self.cidx = cidx
357
355
  self.sorted_columns = {
358
356
  1: True, # Count
359
357
  }
360
358
 
361
359
  df = dftable.df.filter(dftable.visible_rows) if False in dftable.visible_rows else dftable.df
362
360
  self.total_count = len(df)
363
- self.df: pl.DataFrame = df[df.columns[self.col_idx]].value_counts(sort=True).sort("count", descending=True)
361
+ self.df: pl.DataFrame = df[df.columns[self.cidx]].value_counts(sort=True).sort("count", descending=True)
364
362
 
365
363
  def on_mount(self) -> None:
366
364
  """Create the frequency table."""
@@ -369,19 +367,19 @@ class FrequencyScreen(TableScreen):
369
367
  def on_key(self, event):
370
368
  if event.key == "left_square_bracket": # '['
371
369
  # Sort by current column in ascending order
372
- self._sort_by_column(descending=False)
370
+ self.sort_by_column(descending=False)
373
371
  event.stop()
374
372
  elif event.key == "right_square_bracket": # ']'
375
373
  # Sort by current column in descending order
376
- self._sort_by_column(descending=True)
374
+ self.sort_by_column(descending=True)
377
375
  event.stop()
378
376
  elif event.key == "v":
379
377
  # Filter the main table by the selected value
380
- self._filter_or_highlight_selected_value(self._get_col_name_value(), action="filter")
378
+ self.filter_or_view_selected_value(self.get_cidx_name_value(), action="view")
381
379
  event.stop()
382
380
  elif event.key == "quotation_mark": # '"'
383
381
  # Highlight the main table by the selected value
384
- self._filter_or_highlight_selected_value(self._get_col_name_value(), action="highlight")
382
+ self.filter_or_view_selected_value(self.get_cidx_name_value(), action="filter")
385
383
  event.stop()
386
384
 
387
385
  def build_table(self) -> None:
@@ -389,8 +387,8 @@ class FrequencyScreen(TableScreen):
389
387
  self.table.clear(columns=True)
390
388
 
391
389
  # Create frequency table
392
- column = self.dftable.df.columns[self.col_idx]
393
- dtype = self.dftable.df.dtypes[self.col_idx]
390
+ column = self.dftable.df.columns[self.cidx]
391
+ dtype = self.dftable.df.dtypes[self.cidx]
394
392
  dc = DtypeConfig(dtype)
395
393
 
396
394
  # Add column headers with sort indicators
@@ -468,7 +466,7 @@ class FrequencyScreen(TableScreen):
468
466
  key="total",
469
467
  )
470
468
 
471
- def _sort_by_column(self, descending: bool) -> None:
469
+ def sort_by_column(self, descending: bool) -> None:
472
470
  """Sort the dataframe by the selected column and refresh the main table."""
473
471
  row_idx, col_idx = self.table.cursor_coordinate
474
472
  col_sort = col_idx if col_idx == 0 else 1
@@ -493,15 +491,15 @@ class FrequencyScreen(TableScreen):
493
491
  # order = "desc" if descending else "asc"
494
492
  # self.notify(f"Sorted by [on $primary]{col_name}[/] ({order})", title="Sort")
495
493
 
496
- def _get_col_name_value(self) -> tuple[str, str] | None:
494
+ def get_cidx_name_value(self) -> tuple[str, str, str] | None:
497
495
  row_idx = self.table.cursor_row
498
496
  if row_idx >= len(self.df[:, 0]): # first column
499
497
  return None # Skip the last `Total` row
500
498
 
501
- col_name = self.dftable.df.columns[self.col_idx]
502
- col_dtype = self.dftable.df.dtypes[self.col_idx]
499
+ col_name = self.dftable.df.columns[self.cidx]
500
+ col_dtype = self.dftable.df.dtypes[self.cidx]
503
501
 
504
502
  cell_value = self.table.get_cell_at(Coordinate(row_idx, 0))
505
503
  col_value = NULL if cell_value.plain == NULL_DISPLAY else DtypeConfig(col_dtype).convert(cell_value.plain)
506
504
 
507
- return col_name, col_value
505
+ return self.cidx, col_name, col_value
@@ -494,13 +494,13 @@ class FilterScreen(YesNoScreen):
494
494
 
495
495
  CSS = YesNoScreen.DEFAULT_CSS.replace("YesNoScreen", "FilterScreen")
496
496
 
497
- def __init__(self, df: pl.DataFrame, cidx: int, input_value: str | None = None):
497
+ def __init__(self, df: pl.DataFrame, cidx: int, term: str | None = None):
498
498
  self.df = df
499
499
  self.cidx = cidx
500
500
  super().__init__(
501
501
  title="Filter by Expression",
502
502
  label="e.g., NULL, $1 > 50, $name == 'text', $_ > 100, $a < $b, $_.str.contains('sub')",
503
- input=input_value,
503
+ input=term,
504
504
  checkbox="Match Nocase",
505
505
  checkbox2="Match Whole",
506
506
  on_yes_callback=self._get_input,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataframe-textual
3
- Version: 1.9.0
3
+ Version: 1.10.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
@@ -0,0 +1,14 @@
1
+ dataframe_textual/__init__.py,sha256=IFPb8RMUgghw0eRomehkkC684Iny_gs1VkiZMQ5ZpFk,813
2
+ dataframe_textual/__main__.py,sha256=VRH80gWJGLxfURg1GiN3cwWI7O_TQM5PqhqXbUCC1bg,3253
3
+ dataframe_textual/common.py,sha256=fh5dkN06VgFMItgFMlB7BMdiBjalc24jdqaGTJI7yCM,25409
4
+ dataframe_textual/data_frame_help_panel.py,sha256=iEKaur-aH1N_oqHu-vMwEEjfkjQiThK24UO5izsOiW0,3416
5
+ dataframe_textual/data_frame_table.py,sha256=bzKD7PKz2CP7HeZIqUkpDYZHhx7amBaEzHWRlq2i3XY,127578
6
+ dataframe_textual/data_frame_viewer.py,sha256=4fPIj-dvqhoTRxxOPfwTfRpYcfle7VtwTZfP4sJHA6E,21254
7
+ dataframe_textual/sql_screen.py,sha256=Pk2ENyDVkOXBLcsEMeXOEMkdzRH_NJ3T1biVUcY8j4Q,7411
8
+ dataframe_textual/table_screen.py,sha256=IhaGAWjzXaLvvc4JLBlAM6_Hl3t7du7vuHQYhrvW_5o,18055
9
+ dataframe_textual/yes_no_screen.py,sha256=qbuhywLIGBL52zQfSGVZQCfsdSmDz9JH6C4YFLrLYKU,26233
10
+ dataframe_textual-1.10.1.dist-info/METADATA,sha256=F0pCwiggF6y2WVr9Fs4YDgII5Rh-JZHuGeJq1y5AQpA,30381
11
+ dataframe_textual-1.10.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
+ dataframe_textual-1.10.1.dist-info/entry_points.txt,sha256=R_GoooOxcq6ab4RaHiVoZ4zrZJ-phMcGmlL2rwqncW8,107
13
+ dataframe_textual-1.10.1.dist-info/licenses/LICENSE,sha256=AVTg0gk1X-LHI-nnHlAMDQetrwuDZK4eypgSMDO46Yc,1069
14
+ dataframe_textual-1.10.1.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- dataframe_textual/__init__.py,sha256=IFPb8RMUgghw0eRomehkkC684Iny_gs1VkiZMQ5ZpFk,813
2
- dataframe_textual/__main__.py,sha256=VRH80gWJGLxfURg1GiN3cwWI7O_TQM5PqhqXbUCC1bg,3253
3
- dataframe_textual/common.py,sha256=fh5dkN06VgFMItgFMlB7BMdiBjalc24jdqaGTJI7yCM,25409
4
- dataframe_textual/data_frame_help_panel.py,sha256=iEKaur-aH1N_oqHu-vMwEEjfkjQiThK24UO5izsOiW0,3416
5
- dataframe_textual/data_frame_table.py,sha256=GfjknW0xzjEyz6syIW_BvCLBjGdEZpn1jW87PefBlOo,127408
6
- dataframe_textual/data_frame_viewer.py,sha256=WI6e6AujRQkfCk2yerIRhuiTwtn86y1yWg3WesqdmAs,21315
7
- dataframe_textual/sql_screen.py,sha256=9s9V-pcyYvXoaqsW_85hs-JSsllzayqhENC6ETGq7Xw,7464
8
- dataframe_textual/table_screen.py,sha256=t2eNYS4a-G95qxRU9CFSM8kFjm-STIzkcP_o998ilqY,18135
9
- dataframe_textual/yes_no_screen.py,sha256=HF2Z1WgOEof7NdA_u0s3pCxP26n69Kr6sQWwQvjK-YE,26247
10
- dataframe_textual-1.9.0.dist-info/METADATA,sha256=bE00p7b3HACIwJzqeHRt28F99qMyruWKNEaxzm8wGVk,30380
11
- dataframe_textual-1.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
- dataframe_textual-1.9.0.dist-info/entry_points.txt,sha256=R_GoooOxcq6ab4RaHiVoZ4zrZJ-phMcGmlL2rwqncW8,107
13
- dataframe_textual-1.9.0.dist-info/licenses/LICENSE,sha256=AVTg0gk1X-LHI-nnHlAMDQetrwuDZK4eypgSMDO46Yc,1069
14
- dataframe_textual-1.9.0.dist-info/RECORD,,