dbxplay 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.
dbxplay/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ dbxplay — A Databricks-like interactive display() function
3
+ for Jupyter Notebooks, AWS Glue, Google Colab, and VS Code.
4
+
5
+ Usage:
6
+ from dbxplay import display
7
+ display(df)
8
+ """
9
+
10
+ from dbxplay.core import display
11
+
12
+ __version__ = "0.1.0"
13
+ __all__ = ["display"]
dbxplay/core.py ADDED
@@ -0,0 +1,288 @@
1
+ """
2
+ core.py — Main display() function and DataFrame type detection/conversion.
3
+ """
4
+
5
+ import json
6
+ import uuid
7
+ import html as html_module
8
+ import math
9
+ from typing import Any, Dict, List, Optional, Union
10
+
11
+ from dbxplay.templates import render_table_html
12
+
13
+
14
+ def _is_pyspark_dataframe(obj: Any) -> bool:
15
+ """Check if the object is a PySpark DataFrame without importing PySpark."""
16
+ cls_name = type(obj).__name__
17
+ module = type(obj).__module__ or ""
18
+ return cls_name == "DataFrame" and "pyspark" in module
19
+
20
+
21
+ def _is_pandas_dataframe(obj: Any) -> bool:
22
+ """Check if the object is a Pandas DataFrame."""
23
+ cls_name = type(obj).__name__
24
+ module = type(obj).__module__ or ""
25
+ return cls_name == "DataFrame" and "pandas" in module
26
+
27
+
28
+ def _is_polars_dataframe(obj: Any) -> bool:
29
+ """Check if the object is a Polars DataFrame."""
30
+ cls_name = type(obj).__name__
31
+ module = type(obj).__module__ or ""
32
+ return cls_name == "DataFrame" and "polars" in module
33
+
34
+
35
+ def _infer_dtype_category(values: list, col_name: str = "") -> str:
36
+ """Infer a human-readable dtype category from a sample of Python values.
37
+
38
+ Returns one of: 'string', 'integer', 'float', 'boolean', 'datetime', 'complex'
39
+ """
40
+ import datetime as dt
41
+
42
+ non_none = [v for v in values if v is not None]
43
+ if not non_none:
44
+ return "string"
45
+
46
+ sample = non_none[:50]
47
+
48
+ # Check boolean first (bool is subclass of int in Python)
49
+ if all(isinstance(v, bool) for v in sample):
50
+ return "boolean"
51
+ if all(isinstance(v, int) and not isinstance(v, bool) for v in sample):
52
+ return "integer"
53
+ if all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in sample):
54
+ return "float"
55
+ if all(isinstance(v, (dt.datetime, dt.date)) for v in sample):
56
+ return "datetime"
57
+ if all(isinstance(v, (dict, list)) for v in sample):
58
+ return "complex"
59
+
60
+ # Try to detect string-encoded timestamps
61
+ str_vals = [v for v in sample if isinstance(v, str)]
62
+ if str_vals and len(str_vals) == len(sample):
63
+ ts_count = sum(1 for s in str_vals[:10] if len(s) >= 10 and s[4:5] == "-")
64
+ if ts_count >= len(str_vals[:10]) * 0.8:
65
+ return "datetime"
66
+
67
+ return "string"
68
+
69
+
70
+ def _pandas_dtype_to_category(dtype) -> str:
71
+ """Convert a pandas dtype to our category string."""
72
+ dtype_str = str(dtype).lower()
73
+ if "bool" in dtype_str:
74
+ return "boolean"
75
+ if "int" in dtype_str:
76
+ return "integer"
77
+ if "float" in dtype_str or "double" in dtype_str:
78
+ return "float"
79
+ if "datetime" in dtype_str or "timestamp" in dtype_str:
80
+ return "datetime"
81
+ if "object" in dtype_str or "string" in dtype_str or "str" in dtype_str:
82
+ return "string"
83
+ if "category" in dtype_str:
84
+ return "string"
85
+ return "string"
86
+
87
+
88
+ def _spark_dtype_to_category(dtype_str: str) -> str:
89
+ """Convert a PySpark dtype string to our category string."""
90
+ dtype_str = dtype_str.lower()
91
+ if "bool" in dtype_str:
92
+ return "boolean"
93
+ if "int" in dtype_str or "long" in dtype_str or "short" in dtype_str or "byte" in dtype_str:
94
+ return "integer"
95
+ if "float" in dtype_str or "double" in dtype_str or "decimal" in dtype_str:
96
+ return "float"
97
+ if "timestamp" in dtype_str or "date" in dtype_str:
98
+ return "datetime"
99
+ if "struct" in dtype_str or "array" in dtype_str or "map" in dtype_str:
100
+ return "complex"
101
+ return "string"
102
+
103
+
104
+ def _polars_dtype_to_category(dtype) -> str:
105
+ """Convert a Polars dtype to our category string."""
106
+ dtype_str = str(dtype).lower()
107
+ if "bool" in dtype_str:
108
+ return "boolean"
109
+ if "int" in dtype_str or "uint" in dtype_str:
110
+ return "integer"
111
+ if "float" in dtype_str or "decimal" in dtype_str:
112
+ return "float"
113
+ if "date" in dtype_str or "time" in dtype_str or "duration" in dtype_str:
114
+ return "datetime"
115
+ if "struct" in dtype_str or "list" in dtype_str:
116
+ return "complex"
117
+ return "string"
118
+
119
+
120
+ def _safe_str(val: Any) -> str:
121
+ """Convert a value to a display-safe string."""
122
+ if val is None:
123
+ return "null"
124
+ if isinstance(val, bool):
125
+ return str(val).lower()
126
+ if isinstance(val, float):
127
+ if val != val: # NaN check
128
+ return "NaN"
129
+ if val == float("inf"):
130
+ return "Infinity"
131
+ if val == float("-inf"):
132
+ return "-Infinity"
133
+ return str(val)
134
+
135
+
136
+ def _convert_to_records(
137
+ data: Any,
138
+ limit: int = 1000,
139
+ ) -> tuple:
140
+ """Convert various DataFrame types to a list of dicts and column metadata.
141
+
142
+ Returns:
143
+ (records: List[Dict], columns: List[Dict], total_rows: int, truncated: bool)
144
+ Each column dict has keys: 'name', 'dtype_category'
145
+ """
146
+
147
+ if _is_pyspark_dataframe(data):
148
+ total_rows = None
149
+ limited = data.limit(limit)
150
+ pdf = limited.toPandas()
151
+ records = pdf.to_dict("records")
152
+ columns = []
153
+ for field in data.schema.fields:
154
+ columns.append({
155
+ "name": field.name,
156
+ "dtype_category": _spark_dtype_to_category(str(field.dataType)),
157
+ })
158
+ truncated = len(records) >= limit
159
+ return records, columns, total_rows, truncated
160
+
161
+ if _is_pandas_dataframe(data):
162
+ total_rows = len(data)
163
+ truncated = total_rows > limit
164
+ df_sample = data.head(limit) if truncated else data
165
+ records = json.loads(df_sample.to_json(orient="records", date_format="iso", default_handler=str))
166
+ columns = []
167
+ for col_name in data.columns:
168
+ columns.append({
169
+ "name": str(col_name),
170
+ "dtype_category": _pandas_dtype_to_category(data[col_name].dtype),
171
+ })
172
+ return records, columns, total_rows, truncated
173
+
174
+ if _is_polars_dataframe(data):
175
+ total_rows = data.height
176
+ truncated = total_rows > limit
177
+ df_sample = data.head(limit) if truncated else data
178
+ records = df_sample.to_dicts()
179
+ columns = []
180
+ for col_name in data.columns:
181
+ columns.append({
182
+ "name": col_name,
183
+ "dtype_category": _polars_dtype_to_category(data[col_name].dtype),
184
+ })
185
+ return records, columns, total_rows, truncated
186
+
187
+ if isinstance(data, list):
188
+ if not data:
189
+ return [], [], 0, False
190
+ total_rows = len(data)
191
+ truncated = total_rows > limit
192
+ sample = data[:limit]
193
+
194
+ if isinstance(sample[0], dict):
195
+ records = sample
196
+ all_keys = []
197
+ seen = set()
198
+ for row in sample:
199
+ for k in row:
200
+ if k not in seen:
201
+ all_keys.append(k)
202
+ seen.add(k)
203
+ columns = []
204
+ for k in all_keys:
205
+ vals = [row.get(k) for row in sample]
206
+ columns.append({
207
+ "name": str(k),
208
+ "dtype_category": _infer_dtype_category(vals, str(k)),
209
+ })
210
+ return records, columns, total_rows, truncated
211
+ else:
212
+ records = [{"value": v} for v in sample]
213
+ columns = [{"name": "value", "dtype_category": _infer_dtype_category(sample)}]
214
+ return records, columns, total_rows, truncated
215
+
216
+ if isinstance(data, dict):
217
+ col_names = list(data.keys())
218
+ max_len = max((len(v) if isinstance(v, list) else 1) for v in data.values()) if data else 0
219
+ total_rows = max_len
220
+ truncated = total_rows > limit
221
+ actual_len = min(max_len, limit)
222
+ records = []
223
+ for i in range(actual_len):
224
+ row = {}
225
+ for c in col_names:
226
+ vals = data[c]
227
+ if isinstance(vals, list):
228
+ row[c] = vals[i] if i < len(vals) else None
229
+ else:
230
+ row[c] = vals
231
+ records.append(row)
232
+ columns = []
233
+ for c in col_names:
234
+ vals = data[c] if isinstance(data[c], list) else [data[c]]
235
+ columns.append({
236
+ "name": str(c),
237
+ "dtype_category": _infer_dtype_category(vals[:50], str(c)),
238
+ })
239
+ return records, columns, total_rows, truncated
240
+
241
+ raise TypeError(
242
+ f"display() does not support type '{type(data).__name__}'. "
243
+ "Supported types: PySpark DataFrame, Pandas DataFrame, Polars DataFrame, list, dict."
244
+ )
245
+
246
+
247
+ def display(
248
+ data: Any,
249
+ limit: int = 1000,
250
+ title: str = "Table",
251
+ height: Optional[int] = None,
252
+ ) -> None:
253
+ """Display a DataFrame or data structure in an interactive Databricks-style table.
254
+
255
+ Args:
256
+ data: A PySpark, Pandas, or Polars DataFrame, or a list of dicts / dict of lists.
257
+ limit: Maximum number of rows to display (default 1000). PySpark DataFrames
258
+ are automatically limited to avoid OOM.
259
+ title: The tab title shown in the top bar (default "Table").
260
+ height: Optional fixed height in pixels for the table container. If None,
261
+ the table auto-sizes up to ~500px then scrolls.
262
+ """
263
+ from IPython.display import display as ipy_display, HTML
264
+
265
+ records, columns, total_rows, truncated = _convert_to_records(data, limit=limit)
266
+
267
+ # Sanitize all record values for safe JSON embedding
268
+ safe_records = []
269
+ for row in records:
270
+ safe_row = {}
271
+ for k, v in row.items():
272
+ safe_row[k] = _safe_str(v)
273
+ safe_records.append(safe_row)
274
+
275
+ table_id = "db_table_" + uuid.uuid4().hex[:12]
276
+
277
+ html_str = render_table_html(
278
+ table_id=table_id,
279
+ records=safe_records,
280
+ columns=columns,
281
+ total_rows=total_rows,
282
+ truncated=truncated,
283
+ limit=limit,
284
+ title=title,
285
+ height=height,
286
+ )
287
+
288
+ ipy_display(HTML(html_str))
dbxplay/icons.py ADDED
@@ -0,0 +1,171 @@
1
+ """
2
+ SVG icons used in the Databricks-like table UI.
3
+ These are inline SVG strings matching the Databricks visual style.
4
+ """
5
+
6
+
7
+ def get_type_icon_svg(dtype_category: str) -> str:
8
+ """Return an inline SVG icon for the given data type category.
9
+
10
+ Categories: 'string', 'integer', 'float', 'boolean', 'datetime', 'complex'
11
+ """
12
+ icons = {
13
+ # ABC icon for string columns
14
+ "string": (
15
+ '<svg class="db-type-icon" viewBox="0 0 16 16" width="14" height="14" fill="none">'
16
+ '<text x="1" y="12" font-size="7.5" font-weight="700" fill="#5f6368" '
17
+ 'font-family="Arial,sans-serif" letter-spacing="0.5">'
18
+ '<tspan fill="#1a73e8">A</tspan>'
19
+ '<tspan fill="#5f6368">B</tspan>'
20
+ '</text>'
21
+ '<text x="10.5" y="8" font-size="5.5" font-weight="600" fill="#5f6368" '
22
+ 'font-family="Arial,sans-serif">C</text>'
23
+ '</svg>'
24
+ ),
25
+ # 123 icon for integer columns
26
+ "integer": (
27
+ '<svg class="db-type-icon" viewBox="0 0 16 16" width="14" height="14" fill="none">'
28
+ '<text x="0" y="12" font-size="10" font-weight="700" fill="#5f6368" '
29
+ 'font-family="Arial,sans-serif" letter-spacing="-0.5">#</text>'
30
+ '</svg>'
31
+ ),
32
+ # Decimal icon for float columns
33
+ "float": (
34
+ '<svg class="db-type-icon" viewBox="0 0 16 16" width="14" height="14" fill="none">'
35
+ '<text x="0" y="12" font-size="7" font-weight="700" fill="#5f6368" '
36
+ 'font-family="Arial,sans-serif" letter-spacing="-0.2">1.2</text>'
37
+ '</svg>'
38
+ ),
39
+ # T/F icon for boolean columns
40
+ "boolean": (
41
+ '<svg class="db-type-icon" viewBox="0 0 16 16" width="14" height="14" fill="none">'
42
+ '<text x="0" y="12" font-size="7.5" font-weight="700" fill="#5f6368" '
43
+ 'font-family="Arial,sans-serif">'
44
+ '<tspan fill="#1a73e8">T</tspan>'
45
+ '<tspan fill="#5f6368">/F</tspan>'
46
+ '</text>'
47
+ '</svg>'
48
+ ),
49
+ # Calendar icon for datetime columns
50
+ "datetime": (
51
+ '<svg class="db-type-icon" viewBox="0 0 16 16" width="14" height="14" fill="none">'
52
+ '<rect x="1" y="3" width="14" height="12" rx="2" stroke="#5f6368" stroke-width="1.3" fill="none"/>'
53
+ '<line x1="1" y1="7" x2="15" y2="7" stroke="#5f6368" stroke-width="1.3"/>'
54
+ '<line x1="5" y1="1" x2="5" y2="5" stroke="#5f6368" stroke-width="1.3" stroke-linecap="round"/>'
55
+ '<line x1="11" y1="1" x2="11" y2="5" stroke="#5f6368" stroke-width="1.3" stroke-linecap="round"/>'
56
+ '<rect x="4" y="9.5" width="2.5" height="2" rx="0.5" fill="#1a73e8"/>'
57
+ '</svg>'
58
+ ),
59
+ # Braces icon for complex/json columns
60
+ "complex": (
61
+ '<svg class="db-type-icon" viewBox="0 0 16 16" width="14" height="14" fill="none">'
62
+ '<text x="1" y="13" font-size="12" font-weight="400" fill="#5f6368" '
63
+ 'font-family="Arial,sans-serif">{}</text>'
64
+ '</svg>'
65
+ ),
66
+ }
67
+ return icons.get(dtype_category, icons["string"])
68
+
69
+
70
+ def get_sort_icon_svg() -> str:
71
+ """Return the sort arrow SVG for column headers."""
72
+ return (
73
+ '<svg class="db-sort-icon" viewBox="0 0 10 14" width="8" height="12" fill="none">'
74
+ '<path class="db-sort-asc" d="M5 0L9 5H1L5 0Z" fill="#bcc0c4"/>'
75
+ '<path class="db-sort-desc" d="M5 14L1 9H9L5 14Z" fill="#bcc0c4"/>'
76
+ '</svg>'
77
+ )
78
+
79
+
80
+ def get_chevron_down_svg(size: int = 12) -> str:
81
+ """Return a small chevron-down SVG for dropdown indicators."""
82
+ return (
83
+ f'<svg viewBox="0 0 10 6" width="{size}" height="{size}" fill="none" '
84
+ f'style="vertical-align:middle;">'
85
+ f'<path d="M1 1L5 5L9 1" stroke="#444" stroke-width="1.5" '
86
+ f'stroke-linecap="round" stroke-linejoin="round"/>'
87
+ f'</svg>'
88
+ )
89
+
90
+
91
+ def get_plus_svg(size: int = 14) -> str:
92
+ """Return a plus icon SVG."""
93
+ return (
94
+ f'<svg viewBox="0 0 12 12" width="{size}" height="{size}" fill="none">'
95
+ f'<path d="M6 1V11M1 6H11" stroke="#666" stroke-width="1.5" '
96
+ f'stroke-linecap="round"/>'
97
+ f'</svg>'
98
+ )
99
+
100
+
101
+ def get_download_svg() -> str:
102
+ """Return a download icon SVG."""
103
+ return (
104
+ '<svg viewBox="0 0 16 16" width="14" height="14" fill="none">'
105
+ '<path d="M8 2V10M8 10L5 7M8 10L11 7" stroke="#333" stroke-width="1.4" '
106
+ 'stroke-linecap="round" stroke-linejoin="round"/>'
107
+ '<path d="M3 13H13" stroke="#333" stroke-width="1.4" stroke-linecap="round"/>'
108
+ '</svg>'
109
+ )
110
+
111
+
112
+ def get_copy_svg() -> str:
113
+ """Return a copy icon SVG."""
114
+ return (
115
+ '<svg viewBox="0 0 16 16" width="14" height="14" fill="none">'
116
+ '<rect x="5" y="5" width="8" height="9" rx="1.5" stroke="#333" stroke-width="1.2"/>'
117
+ '<path d="M11 5V3.5A1.5 1.5 0 009.5 2H3.5A1.5 1.5 0 002 3.5V10.5A1.5 1.5 0 003.5 12H5" '
118
+ 'stroke="#333" stroke-width="1.2"/>'
119
+ '</svg>'
120
+ )
121
+
122
+
123
+ def get_filter_svg() -> str:
124
+ """Return a filter funnel icon SVG."""
125
+ return (
126
+ '<svg viewBox="0 0 16 16" width="14" height="14" fill="none">'
127
+ '<path d="M2 3H14L10 8.5V12L6 14V8.5L2 3Z" stroke="#333" stroke-width="1.2" '
128
+ 'stroke-linejoin="round"/>'
129
+ '</svg>'
130
+ )
131
+
132
+
133
+ def get_exclude_svg() -> str:
134
+ """Return a circle-slash (exclude) icon SVG."""
135
+ return (
136
+ '<svg viewBox="0 0 16 16" width="14" height="14" fill="none">'
137
+ '<circle cx="8" cy="8" r="6" stroke="#333" stroke-width="1.2"/>'
138
+ '<line x1="3.5" y1="12.5" x2="12.5" y2="3.5" stroke="#333" stroke-width="1.2"/>'
139
+ '</svg>'
140
+ )
141
+
142
+
143
+ def get_panel_svg() -> str:
144
+ """Return a side-panel toggle icon SVG."""
145
+ return (
146
+ '<svg viewBox="0 0 16 16" width="14" height="14" fill="none">'
147
+ '<rect x="1" y="2" width="14" height="12" rx="2" stroke="#333" stroke-width="1.2"/>'
148
+ '<line x1="10" y1="2" x2="10" y2="14" stroke="#333" stroke-width="1.2"/>'
149
+ '</svg>'
150
+ )
151
+
152
+
153
+ def get_search_svg() -> str:
154
+ """Return a search/magnifying glass icon SVG."""
155
+ return (
156
+ '<svg viewBox="0 0 16 16" width="14" height="14" fill="none">'
157
+ '<circle cx="7" cy="7" r="4.5" stroke="#888" stroke-width="1.3"/>'
158
+ '<line x1="10.5" y1="10.5" x2="14" y2="14" stroke="#888" stroke-width="1.3" '
159
+ 'stroke-linecap="round"/>'
160
+ '</svg>'
161
+ )
162
+
163
+
164
+ def get_close_svg(size: int = 12) -> str:
165
+ """Return a small X close icon SVG."""
166
+ return (
167
+ f'<svg viewBox="0 0 10 10" width="{size}" height="{size}" fill="none">'
168
+ f'<path d="M2 2L8 8M8 2L2 8" stroke="#666" stroke-width="1.5" '
169
+ f'stroke-linecap="round"/>'
170
+ f'</svg>'
171
+ )