1
+ import datetime
1
2
import gc
2
3
import pathlib
3
4
import time
4
5
from typing import ClassVar
5
6
6
7
import click
7
8
import polars as pl
9
+ import tzlocal
8
10
from rich .spinner import Spinner
9
11
from rich .style import Style
10
12
from textual import on , work
33
35
34
36
_SHOW_COLUMNS_ID = "showColumns"
35
37
_COLOR_COLUMNS_ID = "colorColumns"
38
+ _TS_COLUMNS_ID = "tsColumns"
39
+
40
+ _TIMEZONE = str (tzlocal .get_localzone ())
36
41
37
42
38
43
class TableWithBookmarks (CustomTable ):
@@ -90,6 +95,24 @@ def _get_row_bg_color_expr(self, cursor_row_idx: int) -> pl.Expr:
90
95
return tmp
91
96
92
97
98
+ _ALREADY_DT = "dt"
99
+
100
+
101
+ def _guess_timestamp_cols (df : pl .DataFrame ):
102
+ date_range = pl .Series (values = [datetime .date (2001 , 1 , 1 ), datetime .date (2042 , 1 , 1 )])
103
+ converts = [(x ,) + tuple (date_range .dt .epoch (x )) for x in ("s" , "ms" , "us" , "ns" )]
104
+
105
+ for col , dtype in df .schema .items ():
106
+ if dtype .is_integer ():
107
+ for suffix , min_val , max_val in converts :
108
+ all_in_range = df .filter ((pl .col (col ) < min_val ) | (pl .col (col ) > max_val )).is_empty ()
109
+ if all_in_range :
110
+ yield (col , suffix )
111
+ break
112
+ elif dtype .is_temporal ():
113
+ yield (col , _ALREADY_DT )
114
+
115
+
93
116
class SpinnerWidget (Static ):
94
117
def __init__ (self , style : str ):
95
118
super ().__init__ ("" )
@@ -265,13 +288,16 @@ class DtBrowser(Widget): # pylint: disable=too-many-public-methods,too-many-ins
265
288
("b" , "toggle_bookmark" , "Add/Del Bookmark" ),
266
289
Binding ("B" , "show_bookmarks" , "Bookmarks" , key_display = "shift+B" ),
267
290
("c" , "column_selector" , "Columns..." ),
291
+ ("t" , "timestamp_selector" , "Timestamps..." ),
268
292
("r" , "toggle_row_detail" , "Toggle Row Detail" ),
269
293
Binding ("C" , "show_colors" , "Colors..." , key_display = "shift+C" ),
270
294
]
271
295
272
296
color_by : reactive [tuple [str , ...]] = reactive (tuple (), init = False )
273
297
visible_columns : reactive [tuple [str , ...]] = reactive (tuple ())
274
298
all_columns : reactive [tuple [str , ...]] = reactive (tuple ())
299
+ timestamp_columns : reactive [tuple [str , ...]] = reactive (tuple ())
300
+ available_timestamp_columns : reactive [tuple [str , ...]] = reactive (tuple ())
275
301
is_filtered = reactive (False )
276
302
cur_row = reactive (0 )
277
303
cur_total_rows = reactive (0 )
@@ -305,10 +331,15 @@ def __init__(self, table_name: str, source_file_or_table: pathlib.Path | pl.Data
305
331
allow_reorder = False , id = _COLOR_COLUMNS_ID , title = "Select columns to color by"
306
332
)
307
333
334
+ self ._ts_cols = dict (_guess_timestamp_cols (self ._original_dt ))
335
+ self ._ts_col_selector = ColumnSelector (id = _TS_COLUMNS_ID , title = "Select epoch timestamp columns" )
336
+ self .available_timestamp_columns = tuple (self ._ts_cols .keys ())
337
+
308
338
# Necessary to prevent the main table from resizing to 0 when the col selectors are mounted and then immediately resizing
309
339
# (apparently that happens when col selector width = auto)
310
340
self ._color_selector .styles .width = 1
311
341
self ._column_selector .styles .width = 1
342
+ self ._ts_col_selector .styles .width = 1
312
343
313
344
self ._row_detail = RowDetail ()
314
345
@@ -425,13 +456,32 @@ async def action_show_colors(self):
425
456
426
457
await self .query_one ("#main_hori" , Horizontal ).mount (self ._color_selector )
427
458
459
+ async def action_timestamp_selector (self ):
460
+ await self .query_one ("#main_hori" , Horizontal ).mount (self ._ts_col_selector )
461
+
428
462
def _set_filtered_dt (self , filtered_dt : pl .DataFrame , filtered_meta : pl .DataFrame , ** kwargs ):
429
463
self ._filtered_dt = filtered_dt
430
464
self ._meta_dt = filtered_meta
431
465
self ._set_active_dt (self ._filtered_dt , ** kwargs )
432
466
433
467
def _set_active_dt (self , active_dt : pl .DataFrame , new_row : int | None = None ):
434
- self ._display_dt = active_dt .select (self .visible_columns )
468
+ active_dt = active_dt .select (self .visible_columns )
469
+ if self .timestamp_columns :
470
+ ordered_cols : list [pl .Expr ] = []
471
+ for col in self .visible_columns :
472
+ ordered_cols .append (pl .col (col ))
473
+ if col in self .timestamp_columns :
474
+ expr = (
475
+ pl .col (col ).dt .epoch ("ns" ).alias (f"{ col } (ns)" )
476
+ if self ._ts_cols [col ] == _ALREADY_DT
477
+ else pl .from_epoch (pl .col (col ), time_unit = self ._ts_cols [col ])
478
+ .dt .convert_time_zone (_TIMEZONE )
479
+ .alias (f"{ col } (Local)" )
480
+ )
481
+ ordered_cols .append (expr )
482
+ active_dt = active_dt .select (ordered_cols )
483
+
484
+ self ._display_dt = active_dt
435
485
self .cur_total_rows = len (self ._display_dt )
436
486
self .watch_active_search (goto = False )
437
487
(table := self .query_one ("#main_table" , CustomTable )).set_dt (self ._display_dt , self ._meta_dt )
@@ -448,6 +498,11 @@ def reorder_columns(self, event: ColumnSelector.ColumnSelectionChanged):
448
498
async def set_color_by (self , event : ColumnSelector .ColumnSelectionChanged ):
449
499
self .color_by = tuple (event .selected_columns )
450
500
501
+ @on (ColumnSelector .ColumnSelectionChanged , f"#{ _TS_COLUMNS_ID } " )
502
+ async def set_timestamp_cols (self , event : ColumnSelector .ColumnSelectionChanged ):
503
+ self .timestamp_columns = tuple (event .selected_columns )
504
+ self ._set_active_dt (self ._filtered_dt )
505
+
451
506
@on (SelectFromTable )
452
507
def enable_select_from_table (self , event : SelectFromTable ):
453
508
self ._select_interest = f"#{ event .interested_widget .id } "
@@ -534,6 +589,9 @@ def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | No
534
589
if action == "show_bookmarks" :
535
590
return self ._bookmarks .has_bookmarks
536
591
592
+ if action == "timestamp_selector" :
593
+ return len (self ._ts_cols ) > 0
594
+
537
595
return True
538
596
539
597
@work (exclusive = True )
@@ -584,6 +642,9 @@ def on_mount(self):
584
642
585
643
def compose (self ) -> ComposeResult :
586
644
"""Create child widgets for the app."""
645
+ self ._ts_col_selector .data_bind (
646
+ selected_columns = DtBrowser .timestamp_columns , available_columns = DtBrowser .available_timestamp_columns
647
+ )
587
648
self ._color_selector .data_bind (selected_columns = DtBrowser .color_by , available_columns = DtBrowser .all_columns )
588
649
self ._column_selector .data_bind (
589
650
selected_columns = DtBrowser .visible_columns , available_columns = DtBrowser .all_columns
0 commit comments