dataframe-textual 0.3.2__py3-none-any.whl → 1.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dataframe_textual/__init__.py +1 -2
- dataframe_textual/__main__.py +62 -14
- dataframe_textual/common.py +587 -92
- dataframe_textual/data_frame_help_panel.py +28 -8
- dataframe_textual/data_frame_table.py +2579 -704
- dataframe_textual/data_frame_viewer.py +215 -179
- dataframe_textual/sql_screen.py +202 -0
- dataframe_textual/table_screen.py +296 -100
- dataframe_textual/yes_no_screen.py +454 -165
- dataframe_textual-1.5.0.dist-info/METADATA +987 -0
- dataframe_textual-1.5.0.dist-info/RECORD +14 -0
- {dataframe_textual-0.3.2.dist-info → dataframe_textual-1.5.0.dist-info}/entry_points.txt +1 -0
- dataframe_textual-0.3.2.dist-info/METADATA +0 -548
- dataframe_textual-0.3.2.dist-info/RECORD +0 -13
- {dataframe_textual-0.3.2.dist-info → dataframe_textual-1.5.0.dist-info}/WHEEL +0 -0
- {dataframe_textual-0.3.2.dist-info → dataframe_textual-1.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""DataFrame Viewer application and utilities."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
import sys
|
|
5
4
|
from functools import partial
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from textwrap import dedent
|
|
@@ -11,9 +10,9 @@ from textual.app import App, ComposeResult
|
|
|
11
10
|
from textual.css.query import NoMatches
|
|
12
11
|
from textual.theme import BUILTIN_THEMES
|
|
13
12
|
from textual.widgets import TabbedContent, TabPane
|
|
14
|
-
from textual.widgets.tabbed_content import
|
|
13
|
+
from textual.widgets.tabbed_content import ContentTabs
|
|
15
14
|
|
|
16
|
-
from .common import
|
|
15
|
+
from .common import Source, get_next_item, load_file
|
|
17
16
|
from .data_frame_help_panel import DataFrameHelpPanel
|
|
18
17
|
from .data_frame_table import DataFrameTable
|
|
19
18
|
from .yes_no_screen import OpenFileScreen, SaveFileScreen
|
|
@@ -27,7 +26,7 @@ class DataFrameViewer(App):
|
|
|
27
26
|
|
|
28
27
|
## 🎯 File & Tab Management
|
|
29
28
|
- **Ctrl+O** - 📁 Add a new tab
|
|
30
|
-
- **Ctrl+
|
|
29
|
+
- **Ctrl+A** - 💾 Save all tabs
|
|
31
30
|
- **Ctrl+W** - ❌ Close current tab
|
|
32
31
|
- **>** or **b** - ▶️ Next tab
|
|
33
32
|
- **<** - ◀️ Previous tab
|
|
@@ -35,7 +34,7 @@ class DataFrameViewer(App):
|
|
|
35
34
|
- **q** - 🚪 Quit application
|
|
36
35
|
|
|
37
36
|
## 🎨 View & Settings
|
|
38
|
-
-
|
|
37
|
+
- **F1** - ❓ Toggle this help panel
|
|
39
38
|
- **k** - 🌙 Cycle through themes
|
|
40
39
|
|
|
41
40
|
## ⭐ Features
|
|
@@ -52,108 +51,141 @@ class DataFrameViewer(App):
|
|
|
52
51
|
|
|
53
52
|
BINDINGS = [
|
|
54
53
|
("q", "quit", "Quit"),
|
|
55
|
-
("
|
|
54
|
+
("f1", "toggle_help_panel", "Help"),
|
|
56
55
|
("B", "toggle_tab_bar", "Toggle Tab Bar"),
|
|
57
56
|
("ctrl+o", "add_tab", "Add Tab"),
|
|
58
|
-
("ctrl+
|
|
57
|
+
("ctrl+a", "save_all_tabs", "Save All Tabs"),
|
|
59
58
|
("ctrl+w", "close_tab", "Close Tab"),
|
|
60
59
|
("greater_than_sign,b", "next_tab(1)", "Next Tab"),
|
|
61
60
|
("less_than_sign", "next_tab(-1)", "Prev Tab"),
|
|
62
61
|
]
|
|
63
62
|
|
|
64
63
|
CSS = """
|
|
65
|
-
TabbedContent {
|
|
66
|
-
height: 100%; /* Or a specific value, e.g., 20; */
|
|
67
|
-
}
|
|
68
64
|
TabbedContent > ContentTabs {
|
|
69
65
|
dock: bottom;
|
|
70
66
|
}
|
|
71
67
|
TabbedContent > ContentSwitcher {
|
|
72
68
|
overflow: auto;
|
|
73
|
-
height: 1fr;
|
|
69
|
+
height: 1fr;
|
|
74
70
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
background: $primary;
|
|
78
|
-
color: $text;
|
|
71
|
+
ContentTab.-active {
|
|
72
|
+
background: $block-cursor-background; /* Same as underline */
|
|
79
73
|
}
|
|
80
74
|
"""
|
|
81
75
|
|
|
82
|
-
def __init__(self, *
|
|
76
|
+
def __init__(self, *sources: Source) -> None:
|
|
77
|
+
"""Initialize the DataFrame Viewer application.
|
|
78
|
+
|
|
79
|
+
Loads data from provided sources and prepares the tabbed interface.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
sources: sources to load dataframes from, each as a tuple of
|
|
83
|
+
(DataFrame, filename, tabname).
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
None
|
|
87
|
+
"""
|
|
83
88
|
super().__init__()
|
|
84
|
-
self.sources =
|
|
89
|
+
self.sources = sources
|
|
85
90
|
self.tabs: dict[TabPane, DataFrameTable] = {}
|
|
86
91
|
self.help_panel = None
|
|
87
92
|
|
|
88
93
|
def compose(self) -> ComposeResult:
|
|
89
|
-
"""
|
|
94
|
+
"""Compose the application widget structure.
|
|
95
|
+
|
|
96
|
+
Creates a tabbed interface with one tab per file/sheet loaded. Each tab
|
|
97
|
+
contains a DataFrameTable widget for displaying and interacting with the data.
|
|
98
|
+
|
|
99
|
+
Yields:
|
|
100
|
+
TabPane: One tab per file or sheet for the tabbed interface.
|
|
101
|
+
"""
|
|
90
102
|
# Tabbed interface
|
|
91
103
|
self.tabbed = TabbedContent(id="main_tabs")
|
|
92
104
|
with self.tabbed:
|
|
93
105
|
seen_names = set()
|
|
94
|
-
for idx,
|
|
106
|
+
for idx, source in enumerate(self.sources, start=1):
|
|
107
|
+
df, filename, tabname = source.frame, source.filename, source.tabname
|
|
108
|
+
tab_id = f"tab_{idx}"
|
|
109
|
+
|
|
110
|
+
if not tabname:
|
|
111
|
+
tabname = Path(filename).stem or tab_id
|
|
112
|
+
|
|
95
113
|
# Ensure unique tab names
|
|
96
|
-
|
|
97
|
-
|
|
114
|
+
counter = 1
|
|
115
|
+
while tabname in seen_names:
|
|
116
|
+
tabname = f"{tabname}_{counter}"
|
|
117
|
+
counter += 1
|
|
98
118
|
seen_names.add(tabname)
|
|
99
119
|
|
|
100
|
-
tab_id = f"tab_{idx}"
|
|
101
120
|
try:
|
|
102
|
-
table = DataFrameTable(
|
|
103
|
-
df, filename, name=tabname, id=tab_id, zebra_stripes=True
|
|
104
|
-
)
|
|
121
|
+
table = DataFrameTable(df, filename, name=tabname, id=tab_id, zebra_stripes=True)
|
|
105
122
|
tab = TabPane(tabname, table, name=tabname, id=tab_id)
|
|
106
123
|
self.tabs[tab] = table
|
|
107
124
|
yield tab
|
|
108
125
|
except Exception as e:
|
|
109
|
-
self.notify(
|
|
126
|
+
self.notify(
|
|
127
|
+
f"Error loading [$error]{filename}[/]: Try [$accent]-I[/] to disable schema inference",
|
|
128
|
+
severity="error",
|
|
129
|
+
)
|
|
130
|
+
self.log(f"Error loading `{filename}`: {str(e)}")
|
|
110
131
|
|
|
111
132
|
def on_mount(self) -> None:
|
|
112
|
-
"""Set up the
|
|
133
|
+
"""Set up the application when it starts.
|
|
134
|
+
|
|
135
|
+
Initializes the app by hiding the tab bar for single-file mode and focusing
|
|
136
|
+
the active table widget.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
None
|
|
140
|
+
"""
|
|
113
141
|
if len(self.tabs) == 1:
|
|
114
142
|
self.query_one(ContentTabs).display = False
|
|
115
143
|
self._get_active_table().focus()
|
|
116
144
|
|
|
117
|
-
def on_key(self, event):
|
|
145
|
+
def on_key(self, event) -> None:
|
|
146
|
+
"""Handle key press events at the application level.
|
|
147
|
+
|
|
148
|
+
Currently handles theme cycling with the 'k' key.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
event: The key event object containing key information.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
None
|
|
155
|
+
"""
|
|
118
156
|
if event.key == "k":
|
|
119
|
-
self.theme =
|
|
120
|
-
self.notify(
|
|
121
|
-
f"Switched to theme: [on $primary]{self.theme}[/]", title="Theme"
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
def on_tabbed_content_tab_activated(
|
|
125
|
-
self, event: TabbedContent.TabActivated
|
|
126
|
-
) -> None:
|
|
127
|
-
"""Handle tab changes (only for multiple tabs)."""
|
|
128
|
-
# Only process if we have multiple files
|
|
129
|
-
if len(self.tabs) <= 1:
|
|
130
|
-
return
|
|
157
|
+
self.theme = get_next_item(list(BUILTIN_THEMES.keys()), self.theme)
|
|
158
|
+
self.notify(f"Switched to theme: [$success]{self.theme}[/]", title="Theme")
|
|
131
159
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
for tab in self.tabbed.query(ContentTab):
|
|
135
|
-
if tab != event.tab:
|
|
136
|
-
tab.remove_class("active")
|
|
160
|
+
def on_tabbed_content_tab_activated(self, event: TabbedContent.TabActivated) -> None:
|
|
161
|
+
"""Handle tab activation events.
|
|
137
162
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if table := self._get_active_table():
|
|
141
|
-
table.focus()
|
|
142
|
-
except NoMatches:
|
|
143
|
-
pass
|
|
163
|
+
When a tab is activated, focuses the table widget and loads its data if not already loaded.
|
|
164
|
+
Applies active styling to the clicked tab and removes it from others.
|
|
144
165
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
166
|
+
Args:
|
|
167
|
+
event: The tab activated event containing the activated tab pane.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
None
|
|
171
|
+
"""
|
|
172
|
+
# Focus the table in the newly activated tab
|
|
173
|
+
if table := self._get_active_table():
|
|
174
|
+
table.focus()
|
|
175
|
+
else:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
if table.loaded_rows == 0:
|
|
179
|
+
table._setup_table()
|
|
154
180
|
|
|
155
181
|
def action_toggle_help_panel(self) -> None:
|
|
156
|
-
"""Toggle the
|
|
182
|
+
"""Toggle the help panel on or off.
|
|
183
|
+
|
|
184
|
+
Shows or hides the context-sensitive help panel. Creates it on first use.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
None
|
|
188
|
+
"""
|
|
157
189
|
if self.help_panel:
|
|
158
190
|
self.help_panel.display = not self.help_panel.display
|
|
159
191
|
else:
|
|
@@ -161,52 +193,140 @@ class DataFrameViewer(App):
|
|
|
161
193
|
self.mount(self.help_panel)
|
|
162
194
|
|
|
163
195
|
def action_add_tab(self) -> None:
|
|
164
|
-
"""Open file
|
|
165
|
-
self.push_screen(OpenFileScreen(), self._handle_file_open)
|
|
196
|
+
"""Open file browser to load a file in a new tab.
|
|
166
197
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
f"Opened: [on $primary]{Path(filename).name}[/]", title="Open"
|
|
175
|
-
)
|
|
176
|
-
except Exception as e:
|
|
177
|
-
self.notify(f"Error: {e}", severity="error")
|
|
198
|
+
Displays the file open dialog for the user to select a file to load
|
|
199
|
+
as a new tab in the interface.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
None
|
|
203
|
+
"""
|
|
204
|
+
self.push_screen(OpenFileScreen(), self._do_add_tab)
|
|
178
205
|
|
|
179
206
|
def action_save_all_tabs(self) -> None:
|
|
180
|
-
"""Save all tabs to a Excel file.
|
|
181
|
-
|
|
207
|
+
"""Save all open tabs to a single Excel file.
|
|
208
|
+
|
|
209
|
+
Displays a save dialog to choose filename and location, then saves all
|
|
210
|
+
open tabs as separate sheets in a single Excel workbook.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
None
|
|
214
|
+
"""
|
|
215
|
+
callback = partial(self._get_active_table()._do_save_file, all_tabs=True)
|
|
182
216
|
self.push_screen(
|
|
183
217
|
SaveFileScreen("all-tabs.xlsx", title="Save All Tabs"),
|
|
184
218
|
callback=callback,
|
|
185
219
|
)
|
|
186
220
|
|
|
187
221
|
def action_close_tab(self) -> None:
|
|
188
|
-
"""Close
|
|
222
|
+
"""Close the currently active tab.
|
|
223
|
+
|
|
224
|
+
Closes the current tab. If this is the only tab, exits the application instead.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
None
|
|
228
|
+
"""
|
|
189
229
|
if len(self.tabs) <= 1:
|
|
190
230
|
self.app.exit()
|
|
191
231
|
return
|
|
192
232
|
self._close_tab()
|
|
193
233
|
|
|
194
|
-
def action_next_tab(self, offset: int = 1) ->
|
|
195
|
-
"""Switch to next tab
|
|
234
|
+
def action_next_tab(self, offset: int = 1) -> None:
|
|
235
|
+
"""Switch to the next tab or previous tab.
|
|
236
|
+
|
|
237
|
+
Cycles through tabs by the specified offset. With offset=1, moves to next tab.
|
|
238
|
+
With offset=-1, moves to previous tab. Wraps around when reaching edges.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
offset: Number of tabs to advance (+1 for next, -1 for previous). Defaults to 1.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
None
|
|
245
|
+
"""
|
|
196
246
|
if len(self.tabs) <= 1:
|
|
197
247
|
return
|
|
198
248
|
try:
|
|
199
249
|
tabs: list[TabPane] = list(self.tabs.keys())
|
|
200
|
-
next_tab =
|
|
250
|
+
next_tab = get_next_item(tabs, self.tabbed.active_pane, offset)
|
|
201
251
|
self.tabbed.active = next_tab.id
|
|
202
252
|
except (NoMatches, ValueError):
|
|
203
253
|
pass
|
|
204
254
|
|
|
205
|
-
def
|
|
206
|
-
"""
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
255
|
+
def action_toggle_tab_bar(self) -> None:
|
|
256
|
+
"""Toggle the tab bar visibility.
|
|
257
|
+
|
|
258
|
+
Shows or hides the tab bar at the bottom of the window. Useful for maximizing
|
|
259
|
+
screen space in single-tab mode.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
None
|
|
263
|
+
"""
|
|
264
|
+
tabs = self.query_one(ContentTabs)
|
|
265
|
+
tabs.display = not tabs.display
|
|
266
|
+
# status = "shown" if tabs.display else "hidden"
|
|
267
|
+
# self.notify(f"Tab bar [$success]{status}[/]", title="Toggle")
|
|
268
|
+
|
|
269
|
+
def _get_active_table(self) -> DataFrameTable | None:
|
|
270
|
+
"""Get the currently active DataFrameTable widget.
|
|
271
|
+
|
|
272
|
+
Retrieves the table from the currently active tab. Returns None if no
|
|
273
|
+
table is found or an error occurs.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
The active DataFrameTable widget, or None if not found.
|
|
277
|
+
"""
|
|
278
|
+
try:
|
|
279
|
+
tabbed: TabbedContent = self.query_one(TabbedContent)
|
|
280
|
+
if active_pane := tabbed.active_pane:
|
|
281
|
+
return active_pane.query_one(DataFrameTable)
|
|
282
|
+
except (NoMatches, AttributeError):
|
|
283
|
+
self.notify("No active table found", title="Locate", severity="error")
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
def _do_add_tab(self, filename: str) -> None:
|
|
287
|
+
"""Add a tab for the opened file.
|
|
288
|
+
|
|
289
|
+
Loads the specified file and creates one or more tabs for it. For Excel files,
|
|
290
|
+
creates one tab per sheet. For other formats, creates a single tab.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
filename: Path to the file to load and add as tab(s).
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
None
|
|
297
|
+
"""
|
|
298
|
+
if filename and os.path.exists(filename):
|
|
299
|
+
try:
|
|
300
|
+
n_tab = 0
|
|
301
|
+
for lf, filename, tabname in load_file(filename, prefix_sheet=True):
|
|
302
|
+
self._add_tab(lf, filename, tabname)
|
|
303
|
+
n_tab += 1
|
|
304
|
+
# self.notify(f"Added [$accent]{n_tab}[/] tab(s) for [$success]{filename}[/]", title="Open")
|
|
305
|
+
except Exception as e:
|
|
306
|
+
self.notify(f"Error loading [$error]{filename}[/]: {str(e)}", title="Open", severity="error")
|
|
307
|
+
else:
|
|
308
|
+
self.notify(f"File does not exist: [$warning]{filename}[/]", title="Open", severity="warning")
|
|
309
|
+
|
|
310
|
+
def _add_tab(self, df: pl.DataFrame, filename: str, tabname: str) -> None:
|
|
311
|
+
"""Add new tab for the given DataFrame.
|
|
312
|
+
|
|
313
|
+
Creates and adds a new tab with the provided DataFrame and configuration.
|
|
314
|
+
Ensures unique tab names by appending an index if needed. Shows the tab bar
|
|
315
|
+
if this is no longer the only tab.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
lf: The Polars DataFrame to display in the new tab.
|
|
319
|
+
filename: The source filename for this data (used in table metadata).
|
|
320
|
+
tabname: The display name for the tab.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
None
|
|
324
|
+
"""
|
|
325
|
+
# Ensure unique tab names
|
|
326
|
+
counter = 1
|
|
327
|
+
while any(tab.name == tabname for tab in self.tabs):
|
|
328
|
+
tabname = f"{tabname}_{counter}"
|
|
329
|
+
counter += 1
|
|
210
330
|
|
|
211
331
|
# Find an available tab index
|
|
212
332
|
tab_idx = f"tab_{len(self.tabs) + 1}"
|
|
@@ -218,9 +338,7 @@ class DataFrameViewer(App):
|
|
|
218
338
|
tab_idx = pending_tab_idx
|
|
219
339
|
break
|
|
220
340
|
|
|
221
|
-
table = DataFrameTable(
|
|
222
|
-
df, filename, zebra_stripes=True, id=tab_idx, name=tabname
|
|
223
|
-
)
|
|
341
|
+
table = DataFrameTable(df, filename, zebra_stripes=True, id=tab_idx, name=tabname)
|
|
224
342
|
tab = TabPane(tabname, table, name=tabname, id=tab_idx)
|
|
225
343
|
self.tabbed.add_pane(tab)
|
|
226
344
|
self.tabs[tab] = table
|
|
@@ -233,7 +351,14 @@ class DataFrameViewer(App):
|
|
|
233
351
|
table.focus()
|
|
234
352
|
|
|
235
353
|
def _close_tab(self) -> None:
|
|
236
|
-
"""Close
|
|
354
|
+
"""Close the currently active tab.
|
|
355
|
+
|
|
356
|
+
Removes the active tab from the interface. If only one tab remains and no more
|
|
357
|
+
can be closed, the application exits instead.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
None
|
|
361
|
+
"""
|
|
237
362
|
try:
|
|
238
363
|
if len(self.tabs) == 1:
|
|
239
364
|
self.app.exit()
|
|
@@ -241,95 +366,6 @@ class DataFrameViewer(App):
|
|
|
241
366
|
if active_pane := self.tabbed.active_pane:
|
|
242
367
|
self.tabbed.remove_pane(active_pane.id)
|
|
243
368
|
self.tabs.pop(active_pane)
|
|
244
|
-
self.notify(
|
|
245
|
-
f"Closed tab [on $primary]{active_pane.name}[/]", title="Close"
|
|
246
|
-
)
|
|
369
|
+
# self.notify(f"Closed tab [$success]{active_pane.name}[/]", title="Close")
|
|
247
370
|
except NoMatches:
|
|
248
371
|
pass
|
|
249
|
-
|
|
250
|
-
def action_toggle_tab_bar(self) -> None:
|
|
251
|
-
"""Toggle tab bar visibility."""
|
|
252
|
-
tabs = self.query_one(ContentTabs)
|
|
253
|
-
tabs.display = not tabs.display
|
|
254
|
-
status = "shown" if tabs.display else "hidden"
|
|
255
|
-
self.notify(f"Tab bar [on $primary]{status}[/]", title="Toggle")
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def _load_dataframe(filenames: list[str]) -> list[tuple[pl.DataFrame, str, str]]:
|
|
259
|
-
"""Load a DataFrame from a file spec.
|
|
260
|
-
|
|
261
|
-
Args:
|
|
262
|
-
filenames: List of filenames to load. If single filename is "-", read from stdin.
|
|
263
|
-
|
|
264
|
-
Returns:
|
|
265
|
-
List of tuples of (DataFrame, filename, tabname)
|
|
266
|
-
"""
|
|
267
|
-
sources = []
|
|
268
|
-
|
|
269
|
-
# Single file
|
|
270
|
-
if len(filenames) == 1:
|
|
271
|
-
filename = filenames[0]
|
|
272
|
-
filepath = Path(filename)
|
|
273
|
-
ext = filepath.suffix.lower()
|
|
274
|
-
|
|
275
|
-
# Handle stdin
|
|
276
|
-
if filename == "-" or not sys.stdin.isatty():
|
|
277
|
-
from io import StringIO
|
|
278
|
-
|
|
279
|
-
# Read CSV from stdin into memory first (stdin is not seekable)
|
|
280
|
-
stdin_data = sys.stdin.read()
|
|
281
|
-
df = pl.read_csv(StringIO(stdin_data))
|
|
282
|
-
|
|
283
|
-
# Reopen stdin to /dev/tty for proper terminal interaction
|
|
284
|
-
try:
|
|
285
|
-
tty = open("/dev/tty")
|
|
286
|
-
os.dup2(tty.fileno(), sys.stdin.fileno())
|
|
287
|
-
except (OSError, FileNotFoundError):
|
|
288
|
-
pass
|
|
289
|
-
|
|
290
|
-
sources.append((df, "stdin.csv", "stdin"))
|
|
291
|
-
# Handle Excel files with multiple sheets
|
|
292
|
-
elif ext in (".xlsx", ".xls"):
|
|
293
|
-
sheets = pl.read_excel(filename, sheet_id=0)
|
|
294
|
-
for sheet_name, df in sheets.items():
|
|
295
|
-
sources.append((df, filename, sheet_name))
|
|
296
|
-
# Handle TSV files
|
|
297
|
-
elif ext in (".tsv", ".tab"):
|
|
298
|
-
df = pl.read_csv(filename, separator="\t")
|
|
299
|
-
sources.append((df, filename, filepath.stem))
|
|
300
|
-
# Handle JSON files
|
|
301
|
-
elif ext == ".json":
|
|
302
|
-
df = pl.read_json(filename)
|
|
303
|
-
sources.append((df, filename, filepath.stem))
|
|
304
|
-
# Handle Parquet files
|
|
305
|
-
elif ext == ".parquet":
|
|
306
|
-
df = pl.read_parquet(filename)
|
|
307
|
-
sources.append((df, filename, filepath.stem))
|
|
308
|
-
# Handle regular CSV files
|
|
309
|
-
else:
|
|
310
|
-
df = pl.read_csv(filename)
|
|
311
|
-
sources.append((df, filename, filepath.stem))
|
|
312
|
-
# Multiple files
|
|
313
|
-
else:
|
|
314
|
-
for filename in filenames:
|
|
315
|
-
filepath = Path(filename)
|
|
316
|
-
ext = filepath.suffix.lower()
|
|
317
|
-
|
|
318
|
-
if ext in (".xlsx", ".xls"):
|
|
319
|
-
# Read only the first sheet for multiple files
|
|
320
|
-
df = pl.read_excel(filename)
|
|
321
|
-
sources.append((df, filename, filepath.stem))
|
|
322
|
-
elif ext in (".tsv", ".tab"):
|
|
323
|
-
df = pl.read_csv(filename, separator="\t")
|
|
324
|
-
sources.append((df, filename, filepath.stem))
|
|
325
|
-
elif ext == ".json":
|
|
326
|
-
df = pl.read_json(filename)
|
|
327
|
-
sources.append((df, filename, filepath.stem))
|
|
328
|
-
elif ext == ".parquet":
|
|
329
|
-
df = pl.read_parquet(filename)
|
|
330
|
-
sources.append((df, filename, filepath.stem))
|
|
331
|
-
else:
|
|
332
|
-
df = pl.read_csv(filename)
|
|
333
|
-
sources.append((df, filename, filepath.stem))
|
|
334
|
-
|
|
335
|
-
return sources
|