dataframe-textual 0.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.

Potentially problematic release.


This version of dataframe-textual might be problematic. Click here for more details.

@@ -0,0 +1,204 @@
1
+ """Common utilities and constants for dataframe_viewer."""
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ import polars as pl
8
+ from rich.text import Text
9
+
10
+ # Boolean string mappings
11
+ BOOLS = {
12
+ "true": True,
13
+ "t": True,
14
+ "yes": True,
15
+ "y": True,
16
+ "1": True,
17
+ "false": False,
18
+ "f": False,
19
+ "no": False,
20
+ "n": False,
21
+ "0": False,
22
+ }
23
+
24
+ # itype is used by Input widget for input validation
25
+ # fmt: off
26
+ STYLES = {
27
+ "Int64": {"style": "cyan", "justify": "right", "itype": "integer", "convert": int},
28
+ "Float64": {"style": "magenta", "justify": "right", "itype": "number", "convert": float},
29
+ "String": {"style": "green", "justify": "left", "itype": "text", "convert": str},
30
+ "Boolean": {"style": "blue", "justify": "center", "itype": "text", "convert": lambda x: BOOLS[x.lower()]},
31
+ "Date": {"style": "blue", "justify": "center", "itype": "text", "convert": str},
32
+ "Datetime": {"style": "blue", "justify": "center", "itype": "text", "convert": str},
33
+ }
34
+ # fmt: on
35
+
36
+
37
+ @dataclass
38
+ class DtypeConfig:
39
+ style: str
40
+ justify: str
41
+ itype: str
42
+ convert: Any
43
+
44
+ def __init__(self, dtype: pl.DataType):
45
+ dc = STYLES.get(
46
+ str(dtype), {"style": "", "justify": "", "itype": "text", "convert": str}
47
+ )
48
+ self.style = dc["style"]
49
+ self.justify = dc["justify"]
50
+ self.itype = dc["itype"]
51
+ self.convert = dc["convert"]
52
+
53
+
54
+ # Subscript digits mapping for sort indicators
55
+ SUBSCRIPT_DIGITS = {
56
+ 0: "₀",
57
+ 1: "₁",
58
+ 2: "₂",
59
+ 3: "₃",
60
+ 4: "₄",
61
+ 5: "₅",
62
+ 6: "₆",
63
+ 7: "₇",
64
+ 8: "₈",
65
+ 9: "₉",
66
+ }
67
+
68
+ # Cursor types ("none" removed)
69
+ CURSOR_TYPES = ["row", "column", "cell"]
70
+
71
+ # Pagination settings
72
+ INITIAL_BATCH_SIZE = 100 # Load this many rows initially
73
+ BATCH_SIZE = 50 # Load this many rows when scrolling
74
+
75
+
76
+ def _format_row(vals, dtypes, apply_justify=True) -> list[Text]:
77
+ """Format a single row with proper styling and justification.
78
+
79
+ Args:
80
+ vals: The list of values in the row.
81
+ dtypes: The list of data types corresponding to each value.
82
+ apply_justify: Whether to apply justification styling. Defaults to True.
83
+ """
84
+ formatted_row = []
85
+
86
+ for val, dtype in zip(vals, dtypes, strict=True):
87
+ dc = DtypeConfig(dtype)
88
+
89
+ # Format the value
90
+ if val is None:
91
+ text_val = "-"
92
+ elif str(dtype).startswith("Float"):
93
+ text_val = f"{val:.4g}"
94
+ else:
95
+ text_val = str(val)
96
+
97
+ formatted_row.append(
98
+ Text(
99
+ text_val,
100
+ style=dc.style,
101
+ justify=dc.justify if apply_justify else "",
102
+ )
103
+ )
104
+
105
+ return formatted_row
106
+
107
+
108
+ def _rindex(lst: list, value) -> int:
109
+ """Return the last index of value in a list. Return -1 if not found."""
110
+ for i, item in enumerate(reversed(lst)):
111
+ if item == value:
112
+ return len(lst) - 1 - i
113
+ return -1
114
+
115
+
116
+ def _next(lst: list[Any], current, offset=1) -> Any:
117
+ """Return the next item in the list after the current item, cycling if needed."""
118
+ if current not in lst:
119
+ raise ValueError("Current item not in list")
120
+ current_index = lst.index(current)
121
+ next_index = (current_index + offset) % len(lst)
122
+ return lst[next_index]
123
+
124
+
125
+ def parse_filter_expression(
126
+ expression: str, df: pl.DataFrame, current_col_idx: int
127
+ ) -> str:
128
+ """Parse and convert a filter expression to Polars syntax.
129
+
130
+ Supports:
131
+ - $_ - Current selected column
132
+ - $1, $2, etc. - Column by 1-based index
133
+ - $col_name - Column by name
134
+ - Comparison operators: ==, !=, <, >, <=, >=
135
+ - Logical operators: &&, ||
136
+ - String literals: 'text', "text"
137
+ - Numeric literals: integers and floats
138
+
139
+ Examples:
140
+ - "$_ > 50" -> "pl.col('current_col') > 50"
141
+ - "$1 > 50" -> "pl.col('col0') > 50"
142
+ - "$name == 'Alex'" -> "pl.col('name') == 'Alex'"
143
+ - "$1 > 3 && $name == 'Alex'" -> "(pl.col('col0') > 3) & (pl.col('name') == 'Alex')"
144
+ - "$age < $salary" -> "pl.col('age') < pl.col('salary')"
145
+
146
+ Args:
147
+ expression: The filter expression as a string.
148
+ df: The DataFrame to validate column references.
149
+ current_col_idx: The index of the currently selected column (0-based). Used for $_ reference.
150
+
151
+ Returns:
152
+ A Python expression string that can be eval'd with Polars symbols.
153
+
154
+ Raises:
155
+ ValueError: If the expression contains invalid column references.
156
+ SyntaxError: If the expression has invalid syntax.
157
+ """
158
+ # Tokenize the expression
159
+ # Pattern matches: $_, $index, $identifier, strings, operators, numbers, etc.
160
+ token_pattern = r'\$_|\$\d+|\$\w+|\'[^\']*\'|"[^"]*"|&&|\|\||<=|>=|!=|==|[+\-*/%<>=()]|\d+\.?\d*|\w+|.'
161
+
162
+ tokens = re.findall(token_pattern, expression)
163
+
164
+ if not tokens:
165
+ raise ValueError("Expression is empty")
166
+
167
+ # Convert tokens to Polars expression syntax
168
+ converted_tokens = []
169
+ for token in tokens:
170
+ if token.startswith("$"):
171
+ # Column reference
172
+ col_ref = token[1:]
173
+
174
+ # Special case: $_ refers to the current selected column
175
+ if col_ref == "_":
176
+ col_name = df.columns[current_col_idx]
177
+ # Check if it's a numeric index
178
+ elif col_ref.isdigit():
179
+ col_idx = int(col_ref) - 1 # Convert to 0-based index
180
+ if col_idx < 0 or col_idx >= len(df.columns):
181
+ raise ValueError(f"Column index out of range: ${col_ref}")
182
+ col_name = df.columns[col_idx]
183
+ else:
184
+ # It's a column name
185
+ if col_ref not in df.columns:
186
+ raise ValueError(f"Column not found: ${col_ref}")
187
+ col_name = col_ref
188
+
189
+ converted_tokens.append(f"pl.col('{col_name}')")
190
+
191
+ elif token in ("&&", "||"):
192
+ # Convert logical operators and wrap surrounding expressions in parentheses
193
+ if token == "&&":
194
+ converted_tokens.append(") & (")
195
+ else:
196
+ converted_tokens.append(") | (")
197
+
198
+ else:
199
+ # Keep as-is (operators, numbers, strings, parentheses)
200
+ converted_tokens.append(token)
201
+
202
+ # Join tokens with space to ensure proper separation
203
+ result = "(" + " ".join(converted_tokens) + ")"
204
+ return result
@@ -0,0 +1,98 @@
1
+ """Help panel widget for displaying context-sensitive help."""
2
+
3
+ from textwrap import dedent
4
+
5
+ from textual.containers import VerticalScroll
6
+ from textual.css.query import NoMatches
7
+ from textual.widget import Widget
8
+ from textual.widgets import Markdown
9
+
10
+
11
+ class DataFrameHelpPanel(Widget):
12
+ """
13
+ Shows context sensitive help for the currently focused widget.
14
+
15
+ Modified from Textual's built-in HelpPanel with KeyPanel removed.
16
+ """
17
+
18
+ DEFAULT_CSS = """
19
+ DataFrameHelpPanel {
20
+ split: right;
21
+ width: 33%;
22
+ min-width: 30;
23
+ max-width: 60;
24
+ border-left: vkey $foreground 30%;
25
+ padding: 0 1;
26
+ height: 1fr;
27
+ padding-right: 1;
28
+ layout: vertical;
29
+ height: 100%;
30
+
31
+ &:ansi {
32
+ background: ansi_default;
33
+ border-left: vkey ansi_black;
34
+
35
+ Markdown {
36
+ background: ansi_default;
37
+ }
38
+ .bindings-table--divide {
39
+ color: transparent;
40
+ }
41
+ }
42
+
43
+ #widget-help {
44
+ height: auto;
45
+ width: 1fr;
46
+ padding: 0;
47
+ margin: 0;
48
+ padding: 1 0;
49
+ margin-top: 1;
50
+ display: none;
51
+ background: $panel;
52
+
53
+ &:ansi {
54
+ background: ansi_default;
55
+ }
56
+
57
+ MarkdownBlock {
58
+ padding-left: 2;
59
+ padding-right: 2;
60
+ }
61
+ }
62
+
63
+ &.-show-help #widget-help {
64
+ display: block;
65
+ }
66
+ }
67
+ """
68
+
69
+ DEFAULT_CLASSES = "-textual-system"
70
+
71
+ def on_mount(self):
72
+ def update_help(focused_widget: Widget | None):
73
+ self.update_help(focused_widget)
74
+
75
+ self.watch(self.screen, "focused", update_help)
76
+
77
+ def update_help(self, focused_widget: Widget | None) -> None:
78
+ """Update the help for the focused widget.
79
+
80
+ Args:
81
+ focused_widget: The currently focused widget, or `None` if no widget was focused.
82
+ """
83
+ if not self.app.app_focus:
84
+ return
85
+ if not self.screen.is_active:
86
+ return
87
+ self.set_class(focused_widget is not None, "-show-help")
88
+ if focused_widget is not None:
89
+ help = self.app.HELP + "\n" + focused_widget.HELP or ""
90
+ if not help:
91
+ self.remove_class("-show-help")
92
+ try:
93
+ self.query_one(Markdown).update(dedent(help))
94
+ except NoMatches:
95
+ pass
96
+
97
+ def compose(self):
98
+ yield VerticalScroll(Markdown(id="widget-help"))