parqv 0.2.0__py3-none-any.whl → 0.2.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.
- parqv/__init__.py +31 -0
- parqv/app.py +84 -102
- parqv/cli.py +112 -0
- parqv/core/__init__.py +31 -0
- parqv/core/config.py +25 -0
- parqv/core/file_utils.py +88 -0
- parqv/core/handler_factory.py +89 -0
- parqv/core/logging.py +46 -0
- parqv/data_sources/__init__.py +44 -0
- parqv/data_sources/base/__init__.py +28 -0
- parqv/data_sources/base/exceptions.py +38 -0
- parqv/{handlers/base_handler.py → data_sources/base/handler.py} +54 -25
- parqv/{handlers → data_sources/formats}/__init__.py +8 -5
- parqv/{handlers → data_sources/formats}/json.py +31 -32
- parqv/{handlers → data_sources/formats}/parquet.py +40 -56
- parqv/views/__init__.py +38 -0
- parqv/views/base.py +98 -0
- parqv/views/components/__init__.py +13 -0
- parqv/views/components/enhanced_data_table.py +152 -0
- parqv/views/components/error_display.py +72 -0
- parqv/views/components/loading_display.py +44 -0
- parqv/views/data_view.py +119 -46
- parqv/views/metadata_view.py +57 -20
- parqv/views/schema_view.py +190 -200
- parqv/views/utils/__init__.py +13 -0
- parqv/views/utils/data_formatters.py +162 -0
- parqv/views/utils/stats_formatters.py +160 -0
- {parqv-0.2.0.dist-info → parqv-0.2.1.dist-info}/METADATA +2 -2
- parqv-0.2.1.dist-info/RECORD +34 -0
- {parqv-0.2.0.dist-info → parqv-0.2.1.dist-info}/WHEEL +1 -1
- parqv-0.2.0.dist-info/RECORD +0 -17
- {parqv-0.2.0.dist-info → parqv-0.2.1.dist-info}/entry_points.txt +0 -0
- {parqv-0.2.0.dist-info → parqv-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {parqv-0.2.0.dist-info → parqv-0.2.1.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,3 @@
|
|
1
|
-
import logging
|
2
1
|
from pathlib import Path
|
3
2
|
from typing import Any, Dict, List, Tuple, Optional, Union
|
4
3
|
|
@@ -7,9 +6,7 @@ import pyarrow as pa
|
|
7
6
|
import pyarrow.compute as pc
|
8
7
|
import pyarrow.parquet as pq
|
9
8
|
|
10
|
-
from
|
11
|
-
|
12
|
-
log = logging.getLogger(__name__)
|
9
|
+
from ..base import DataHandler, DataHandlerError
|
13
10
|
|
14
11
|
|
15
12
|
class ParquetHandlerError(DataHandlerError):
|
@@ -49,17 +46,17 @@ class ParquetHandler(DataHandler):
|
|
49
46
|
self.pq_file = pq.ParquetFile(self.file_path)
|
50
47
|
self.schema = self.pq_file.schema_arrow
|
51
48
|
self.metadata = self.pq_file.metadata
|
52
|
-
|
49
|
+
self.logger.info(f"Successfully initialized ParquetHandler for: {self.file_path.name}")
|
53
50
|
|
54
51
|
except FileNotFoundError as fnf_e:
|
55
|
-
|
52
|
+
self.logger.error(f"File not found during ParquetHandler initialization: {fnf_e}")
|
56
53
|
raise ParquetHandlerError(str(fnf_e)) from fnf_e
|
57
54
|
except pa.lib.ArrowIOError as arrow_io_e:
|
58
|
-
|
55
|
+
self.logger.error(f"Arrow IO Error initializing ParquetHandler for {self.file_path.name}: {arrow_io_e}")
|
59
56
|
raise ParquetHandlerError(
|
60
57
|
f"Failed to open Parquet file '{self.file_path.name}': {arrow_io_e}") from arrow_io_e
|
61
58
|
except Exception as e:
|
62
|
-
|
59
|
+
self.logger.exception(f"Unexpected error initializing ParquetHandler for {self.file_path.name}")
|
63
60
|
self.close()
|
64
61
|
raise ParquetHandlerError(f"Failed to initialize Parquet handler '{self.file_path.name}': {e}") from e
|
65
62
|
|
@@ -71,10 +68,10 @@ class ParquetHandler(DataHandler):
|
|
71
68
|
# ParquetFile might not have a close method depending on source, check first
|
72
69
|
if hasattr(self.pq_file, 'close'):
|
73
70
|
self.pq_file.close()
|
74
|
-
|
71
|
+
self.logger.info(f"Closed Parquet file: {self.file_path.name}")
|
75
72
|
except Exception as e:
|
76
73
|
# Log error during close but don't raise, as we're cleaning up
|
77
|
-
|
74
|
+
self.logger.warning(f"Exception while closing Parquet file {self.file_path.name}: {e}")
|
78
75
|
finally:
|
79
76
|
self.pq_file = None
|
80
77
|
self.schema = None
|
@@ -102,7 +99,7 @@ class ParquetHandler(DataHandler):
|
|
102
99
|
A dictionary containing key metadata attributes, or an error dictionary.
|
103
100
|
"""
|
104
101
|
if not self.metadata or not self.schema:
|
105
|
-
|
102
|
+
self.logger.warning(f"Metadata or schema not available for summary: {self.file_path.name}")
|
106
103
|
return {"error": "Metadata or schema not available"}
|
107
104
|
|
108
105
|
try:
|
@@ -126,7 +123,7 @@ class ParquetHandler(DataHandler):
|
|
126
123
|
|
127
124
|
return summary
|
128
125
|
except Exception as e:
|
129
|
-
|
126
|
+
self.logger.exception(f"Error generating metadata summary for {self.file_path.name}")
|
130
127
|
return {"error": f"Error getting metadata summary: {e}"}
|
131
128
|
|
132
129
|
def get_schema_data(self) -> Optional[List[Dict[str, Any]]]:
|
@@ -138,7 +135,7 @@ class ParquetHandler(DataHandler):
|
|
138
135
|
or None if the schema is unavailable.
|
139
136
|
"""
|
140
137
|
if not self.schema:
|
141
|
-
|
138
|
+
self.logger.warning(f"Schema is not available for get_schema_data: {self.file_path.name}")
|
142
139
|
return None
|
143
140
|
|
144
141
|
schema_list = []
|
@@ -151,7 +148,7 @@ class ParquetHandler(DataHandler):
|
|
151
148
|
"nullable": field.nullable
|
152
149
|
})
|
153
150
|
except Exception as e:
|
154
|
-
|
151
|
+
self.logger.error(f"Error processing field '{field.name}' for schema data: {e}", exc_info=True)
|
155
152
|
schema_list.append({
|
156
153
|
"name": field.name,
|
157
154
|
"type": f"[Error: {e}]",
|
@@ -172,11 +169,11 @@ class ParquetHandler(DataHandler):
|
|
172
169
|
Returns a DataFrame with an 'error' column on failure.
|
173
170
|
"""
|
174
171
|
if not self.pq_file:
|
175
|
-
|
172
|
+
self.logger.warning(f"ParquetFile handler not available for data preview: {self.file_path.name}")
|
176
173
|
return pd.DataFrame({"error": ["Parquet handler not initialized or closed."]})
|
177
174
|
|
178
175
|
if self.metadata and self.metadata.num_rows == 0:
|
179
|
-
|
176
|
+
self.logger.info(f"Parquet file is empty based on metadata: {self.file_path.name}")
|
180
177
|
if self.schema:
|
181
178
|
return pd.DataFrame(columns=self.schema.names)
|
182
179
|
else:
|
@@ -206,10 +203,10 @@ class ParquetHandler(DataHandler):
|
|
206
203
|
if not batches:
|
207
204
|
# Check if file might have rows but reading yielded nothing
|
208
205
|
if self.metadata and self.metadata.num_rows > 0:
|
209
|
-
|
206
|
+
self.logger.warning(
|
210
207
|
f"No batches read for preview, though metadata indicates {self.metadata.num_rows} rows: {self.file_path.name}")
|
211
208
|
else:
|
212
|
-
|
209
|
+
self.logger.info(f"No data read for preview (file likely empty): {self.file_path.name}")
|
213
210
|
# Return empty DF with columns if schema available
|
214
211
|
if self.schema:
|
215
212
|
return pd.DataFrame(columns=self.schema.names)
|
@@ -223,11 +220,11 @@ class ParquetHandler(DataHandler):
|
|
223
220
|
self_destruct=True,
|
224
221
|
types_mapper=pd.ArrowDtype
|
225
222
|
)
|
226
|
-
|
223
|
+
self.logger.info(f"Generated preview of {len(df)} rows for {self.file_path.name}")
|
227
224
|
return df
|
228
225
|
|
229
226
|
except Exception as e:
|
230
|
-
|
227
|
+
self.logger.exception(f"Error generating data preview from Parquet file: {self.file_path.name}")
|
231
228
|
return pd.DataFrame({"error": [f"Failed to fetch preview: {e}"]})
|
232
229
|
|
233
230
|
def get_column_stats(self, column_name: str) -> Dict[str, Any]:
|
@@ -242,13 +239,13 @@ class ParquetHandler(DataHandler):
|
|
242
239
|
and potential error or message keys.
|
243
240
|
"""
|
244
241
|
if not self.pq_file or not self.schema:
|
245
|
-
|
242
|
+
self.logger.warning(f"Parquet file/schema unavailable for column stats: {self.file_path.name}")
|
246
243
|
return self._create_stats_result(column_name, None, error="File or schema not available")
|
247
244
|
|
248
245
|
try:
|
249
246
|
field = self.schema.field(column_name)
|
250
247
|
except KeyError:
|
251
|
-
|
248
|
+
self.logger.warning(f"Column '{column_name}' not found in schema: {self.file_path.name}")
|
252
249
|
return self._create_stats_result(column_name, None, error=f"Column '{column_name}' not found in schema")
|
253
250
|
|
254
251
|
calculated_stats: Dict[str, Any] = {}
|
@@ -261,7 +258,7 @@ class ParquetHandler(DataHandler):
|
|
261
258
|
# Data Reading
|
262
259
|
table = self.pq_file.read(columns=[column_name])
|
263
260
|
column_data = table.column(0)
|
264
|
-
|
261
|
+
self.logger.debug(
|
265
262
|
f"Finished reading column '{column_name}'. Rows: {len(column_data)}, Nulls: {column_data.null_count}")
|
266
263
|
|
267
264
|
# Basic Counts
|
@@ -274,14 +271,14 @@ class ParquetHandler(DataHandler):
|
|
274
271
|
calculated_stats["Null Count"] = f"{null_count:,}"
|
275
272
|
calculated_stats["Null Percentage"] = f"{(null_count / total_count * 100):.2f}%"
|
276
273
|
else:
|
277
|
-
|
274
|
+
self.logger.info(f"Column '{column_name}' read resulted in 0 rows.")
|
278
275
|
message = "Column is empty (0 rows)."
|
279
276
|
valid_count = 0 # Ensure valid_count is 0 for later checks
|
280
277
|
|
281
278
|
# Type-Specific Calculations
|
282
279
|
if valid_count > 0:
|
283
280
|
col_type = field.type
|
284
|
-
|
281
|
+
self.logger.debug(f"Calculating stats for type: {self._format_pyarrow_type(col_type)}")
|
285
282
|
try:
|
286
283
|
if pa.types.is_floating(col_type) or pa.types.is_integer(col_type):
|
287
284
|
calculated_stats.update(self._calculate_numeric_stats(column_data))
|
@@ -300,11 +297,11 @@ class ParquetHandler(DataHandler):
|
|
300
297
|
calculated_stats.update(self._calculate_complex_type_stats(column_data, col_type))
|
301
298
|
message = f"Basic aggregate stats (min/max/mean) not applicable for complex type '{self._format_pyarrow_type(col_type)}'."
|
302
299
|
else:
|
303
|
-
|
300
|
+
self.logger.warning(f"Statistics calculation not fully implemented for type: {col_type}")
|
304
301
|
message = f"Statistics calculation not implemented for type '{self._format_pyarrow_type(col_type)}'."
|
305
302
|
|
306
303
|
except Exception as calc_err:
|
307
|
-
|
304
|
+
self.logger.exception(f"Error during type-specific calculation for column '{column_name}': {calc_err}")
|
308
305
|
error_msg = f"Calculation error for type {field.type}: {calc_err}"
|
309
306
|
calculated_stats["Calculation Error"] = str(calc_err) # Add specific error key
|
310
307
|
|
@@ -315,10 +312,10 @@ class ParquetHandler(DataHandler):
|
|
315
312
|
metadata_stats, metadata_stats_error = self._get_stats_from_metadata(column_name)
|
316
313
|
|
317
314
|
except pa.lib.ArrowException as arrow_e:
|
318
|
-
|
315
|
+
self.logger.exception(f"Arrow error during stats processing for column '{column_name}': {arrow_e}")
|
319
316
|
error_msg = f"Arrow processing error: {arrow_e}"
|
320
317
|
except Exception as e:
|
321
|
-
|
318
|
+
self.logger.exception(f"Unexpected error during stats calculation for column '{column_name}'")
|
322
319
|
error_msg = f"Calculation failed unexpectedly: {e}"
|
323
320
|
|
324
321
|
return self._create_stats_result(
|
@@ -331,7 +328,7 @@ class ParquetHandler(DataHandler):
|
|
331
328
|
try:
|
332
329
|
return value.decode('utf-8', errors='replace')
|
333
330
|
except Exception as e:
|
334
|
-
|
331
|
+
self.logger.warning(f"Could not decode metadata bytes: {e}. Value: {value!r}")
|
335
332
|
return f"[Decode Error: {value!r}]"
|
336
333
|
return str(value) if value is not None else None
|
337
334
|
|
@@ -348,7 +345,7 @@ class ParquetHandler(DataHandler):
|
|
348
345
|
decoded_kv[key_str] = val_str
|
349
346
|
return decoded_kv
|
350
347
|
except Exception as e:
|
351
|
-
|
348
|
+
self.logger.warning(f"Could not decode key-value metadata: {e}")
|
352
349
|
return {"error": f"Error decoding key-value metadata: {e}"}
|
353
350
|
|
354
351
|
def _format_pyarrow_type(self, field_type: pa.DataType) -> str:
|
@@ -438,23 +435,10 @@ class ParquetHandler(DataHandler):
|
|
438
435
|
return stats
|
439
436
|
|
440
437
|
def _calculate_string_binary_stats(self, column_data: pa.ChunkedArray) -> Dict[str, Any]:
|
441
|
-
"""Calculates distinct count
|
438
|
+
"""Calculates distinct count for string/binary columns."""
|
442
439
|
stats: Dict[str, Any] = {}
|
443
440
|
distinct_val, err = self._safe_compute(pc.count_distinct, column_data)
|
444
441
|
stats["Distinct Count"] = f"{distinct_val:,}" if distinct_val is not None and err is None else (err or "N/A")
|
445
|
-
|
446
|
-
if pa.types.is_string(column_data.type) or pa.types.is_large_string(column_data.type):
|
447
|
-
lengths, err_len = self._safe_compute(pc.binary_length, column_data)
|
448
|
-
if err_len is None and lengths is not None:
|
449
|
-
min_len, err_min = self._safe_compute(pc.min, lengths)
|
450
|
-
stats["Min Length"] = min_len if err_min is None else err_min
|
451
|
-
max_len, err_max = self._safe_compute(pc.max, lengths)
|
452
|
-
stats["Max Length"] = max_len if err_max is None else err_max
|
453
|
-
avg_len, err_avg = self._safe_compute(pc.mean, lengths)
|
454
|
-
stats["Avg Length"] = f"{avg_len:.2f}" if avg_len is not None and err_avg is None else (
|
455
|
-
err_avg or "N/A")
|
456
|
-
else:
|
457
|
-
stats.update({"Min Length": "Error", "Max Length": "Error", "Avg Length": "Error"})
|
458
442
|
return stats
|
459
443
|
|
460
444
|
def _calculate_boolean_stats(self, column_data: pa.ChunkedArray) -> Dict[str, Any]:
|
@@ -480,7 +464,7 @@ class ParquetHandler(DataHandler):
|
|
480
464
|
if 'False' not in stats["Value Counts"]: stats["Value Counts"]['False'] = "0"
|
481
465
|
|
482
466
|
except Exception as vc_e:
|
483
|
-
|
467
|
+
self.logger.warning(f"Boolean value count calculation error: {vc_e}", exc_info=True)
|
484
468
|
stats["Value Counts"] = "Error calculating"
|
485
469
|
return stats
|
486
470
|
|
@@ -490,7 +474,7 @@ class ParquetHandler(DataHandler):
|
|
490
474
|
try:
|
491
475
|
unwrapped_data = column_data.dictionary_decode()
|
492
476
|
value_type = col_type.value_type
|
493
|
-
|
477
|
+
self.logger.debug(f"Calculating dictionary stats based on value type: {value_type}")
|
494
478
|
|
495
479
|
# Delegate calculation based on the *value* type
|
496
480
|
if pa.types.is_floating(value_type) or pa.types.is_integer(value_type):
|
@@ -511,10 +495,10 @@ class ParquetHandler(DataHandler):
|
|
511
495
|
err or "N/A")
|
512
496
|
|
513
497
|
except pa.lib.ArrowException as arrow_decode_err:
|
514
|
-
|
498
|
+
self.logger.warning(f"Arrow error decoding dictionary type for stats: {arrow_decode_err}")
|
515
499
|
stats["Dictionary Error"] = f"Decode Error: {arrow_decode_err}"
|
516
500
|
except Exception as dict_e:
|
517
|
-
|
501
|
+
self.logger.warning(f"Could not process dictionary type for stats: {dict_e}")
|
518
502
|
stats["Dictionary Error"] = f"Processing Error: {dict_e}"
|
519
503
|
return stats
|
520
504
|
|
@@ -545,17 +529,17 @@ class ParquetHandler(DataHandler):
|
|
545
529
|
rg_meta = self.metadata.row_group(i)
|
546
530
|
metadata_stats[group_key] = self._extract_stats_for_single_group(rg_meta, col_index)
|
547
531
|
except IndexError:
|
548
|
-
|
532
|
+
self.logger.warning(f"Column index {col_index} out of bounds for row group {i}.")
|
549
533
|
metadata_stats[group_key] = "Index Error"
|
550
534
|
except Exception as e:
|
551
|
-
|
535
|
+
self.logger.warning(f"Error processing metadata stats for RG {i}, column '{column_name}': {e}")
|
552
536
|
metadata_stats[group_key] = f"Read Error: {e}"
|
553
537
|
|
554
538
|
except KeyError:
|
555
|
-
|
539
|
+
self.logger.warning(f"Column '{column_name}' not found in schema for metadata stats.")
|
556
540
|
error_str = f"Column '{column_name}' not found in schema"
|
557
541
|
except Exception as e:
|
558
|
-
|
542
|
+
self.logger.exception(f"Failed to get metadata statistics structure for column '{column_name}'.")
|
559
543
|
error_str = f"Error accessing metadata structure: {e}"
|
560
544
|
|
561
545
|
return metadata_stats, error_str
|
@@ -587,10 +571,10 @@ class ParquetHandler(DataHandler):
|
|
587
571
|
col_chunk_meta.total_uncompressed_size is not None),
|
588
572
|
}
|
589
573
|
except IndexError:
|
590
|
-
|
574
|
+
self.logger.warning(f"Column index {col_index} out of bounds for row group {rg_meta.num_columns} columns.")
|
591
575
|
return "Index Error"
|
592
576
|
except Exception as e:
|
593
|
-
|
577
|
+
self.logger.error(f"Error reading column chunk metadata stats for index {col_index}: {e}", exc_info=True)
|
594
578
|
return f"Metadata Read Error: {e}"
|
595
579
|
|
596
580
|
def _create_stats_result(
|
@@ -613,7 +597,7 @@ class ParquetHandler(DataHandler):
|
|
613
597
|
col_type_str = self._format_pyarrow_type(field.type)
|
614
598
|
col_nullable = field.nullable
|
615
599
|
except Exception as e:
|
616
|
-
|
600
|
+
self.logger.error(f"Error formatting type for column {column_name}: {e}")
|
617
601
|
col_type_str = f"[Error formatting: {field.type}]"
|
618
602
|
col_nullable = None
|
619
603
|
|
parqv/views/__init__.py
CHANGED
@@ -0,0 +1,38 @@
|
|
1
|
+
"""
|
2
|
+
Views package for parqv application.
|
3
|
+
|
4
|
+
This package contains all UI views and their supporting components and utilities.
|
5
|
+
"""
|
6
|
+
|
7
|
+
# Main views
|
8
|
+
from .metadata_view import MetadataView
|
9
|
+
from .data_view import DataView
|
10
|
+
from .schema_view import SchemaView
|
11
|
+
|
12
|
+
# Base classes
|
13
|
+
from .base import BaseView
|
14
|
+
|
15
|
+
# Components (optional, for advanced usage)
|
16
|
+
from .components import ErrorDisplay, LoadingDisplay, EnhancedDataTable
|
17
|
+
|
18
|
+
# Utilities (optional, for advanced usage)
|
19
|
+
from .utils import format_metadata_for_display, format_stats_for_display
|
20
|
+
|
21
|
+
__all__ = [
|
22
|
+
# Main views - these are the primary exports
|
23
|
+
"MetadataView",
|
24
|
+
"DataView",
|
25
|
+
"SchemaView",
|
26
|
+
|
27
|
+
# Base class - for extending functionality
|
28
|
+
"BaseView",
|
29
|
+
|
30
|
+
# Components - for custom view development
|
31
|
+
"ErrorDisplay",
|
32
|
+
"LoadingDisplay",
|
33
|
+
"EnhancedDataTable",
|
34
|
+
|
35
|
+
# Utilities - for data formatting
|
36
|
+
"format_metadata_for_display",
|
37
|
+
"format_stats_for_display",
|
38
|
+
]
|
parqv/views/base.py
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
"""
|
2
|
+
Base classes for parqv views.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Optional
|
6
|
+
|
7
|
+
from textual.containers import Container
|
8
|
+
from textual.widgets import Static
|
9
|
+
|
10
|
+
from ..core import get_logger
|
11
|
+
from ..data_sources import DataHandler
|
12
|
+
|
13
|
+
|
14
|
+
class BaseView(Container):
|
15
|
+
"""
|
16
|
+
Base class for all parqv views.
|
17
|
+
|
18
|
+
Provides common functionality for data loading, error handling,
|
19
|
+
and handler access.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __init__(self, **kwargs):
|
23
|
+
super().__init__(**kwargs)
|
24
|
+
self._is_mounted = False
|
25
|
+
|
26
|
+
@property
|
27
|
+
def logger(self):
|
28
|
+
"""Get a logger for this view."""
|
29
|
+
return get_logger(f"{self.__class__.__module__}.{self.__class__.__name__}")
|
30
|
+
|
31
|
+
@property
|
32
|
+
def handler(self) -> Optional[DataHandler]:
|
33
|
+
"""Get the data handler from the app."""
|
34
|
+
if hasattr(self.app, 'handler'):
|
35
|
+
return self.app.handler
|
36
|
+
return None
|
37
|
+
|
38
|
+
def on_mount(self) -> None:
|
39
|
+
"""Called when the view is mounted."""
|
40
|
+
self._is_mounted = True
|
41
|
+
self.load_content()
|
42
|
+
|
43
|
+
def load_content(self) -> None:
|
44
|
+
"""
|
45
|
+
Load the main content for this view. Must be implemented by subclasses.
|
46
|
+
|
47
|
+
Raises:
|
48
|
+
NotImplementedError: If not implemented by subclass
|
49
|
+
"""
|
50
|
+
raise NotImplementedError("Subclasses must implement load_content()")
|
51
|
+
|
52
|
+
def clear_content(self) -> None:
|
53
|
+
"""Clear all content from the view."""
|
54
|
+
try:
|
55
|
+
self.query("*").remove()
|
56
|
+
except Exception as e:
|
57
|
+
self.logger.error(f"Error clearing content: {e}")
|
58
|
+
|
59
|
+
def show_error(self, message: str, exception: Optional[Exception] = None) -> None:
|
60
|
+
"""
|
61
|
+
Display an error message in the view.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
message: Error message to display
|
65
|
+
exception: Optional exception that caused the error
|
66
|
+
"""
|
67
|
+
if exception:
|
68
|
+
self.logger.exception(f"Error in {self.__class__.__name__}: {message}")
|
69
|
+
else:
|
70
|
+
self.logger.error(f"Error in {self.__class__.__name__}: {message}")
|
71
|
+
|
72
|
+
self.clear_content()
|
73
|
+
error_widget = Static(f"[red]Error: {message}[/red]", classes="error-content")
|
74
|
+
self.mount(error_widget)
|
75
|
+
|
76
|
+
def show_info(self, message: str) -> None:
|
77
|
+
"""
|
78
|
+
Display an informational message in the view.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
message: Info message to display
|
82
|
+
"""
|
83
|
+
self.logger.info(f"Info in {self.__class__.__name__}: {message}")
|
84
|
+
self.clear_content()
|
85
|
+
info_widget = Static(f"[blue]Info: {message}[/blue]", classes="info-content")
|
86
|
+
self.mount(info_widget)
|
87
|
+
|
88
|
+
def check_handler_available(self) -> bool:
|
89
|
+
"""
|
90
|
+
Check if handler is available and show error if not.
|
91
|
+
|
92
|
+
Returns:
|
93
|
+
True if handler is available, False otherwise
|
94
|
+
"""
|
95
|
+
if not self.handler:
|
96
|
+
self.show_error("Data handler not available")
|
97
|
+
return False
|
98
|
+
return True
|
@@ -0,0 +1,13 @@
|
|
1
|
+
"""
|
2
|
+
Reusable UI components for parqv views.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from .error_display import ErrorDisplay
|
6
|
+
from .loading_display import LoadingDisplay
|
7
|
+
from .enhanced_data_table import EnhancedDataTable
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
"ErrorDisplay",
|
11
|
+
"LoadingDisplay",
|
12
|
+
"EnhancedDataTable",
|
13
|
+
]
|
@@ -0,0 +1,152 @@
|
|
1
|
+
"""
|
2
|
+
Enhanced data table component for parqv views.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Optional, List, Tuple, Any
|
6
|
+
|
7
|
+
import pandas as pd
|
8
|
+
from textual.containers import Container
|
9
|
+
from textual.widgets import DataTable, Static
|
10
|
+
|
11
|
+
from ...core import get_logger
|
12
|
+
|
13
|
+
log = get_logger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
class EnhancedDataTable(Container):
|
17
|
+
"""
|
18
|
+
An enhanced data table component that handles DataFrame display with better error handling.
|
19
|
+
"""
|
20
|
+
|
21
|
+
def __init__(self, **kwargs):
|
22
|
+
super().__init__(**kwargs)
|
23
|
+
self._table: Optional[DataTable] = None
|
24
|
+
|
25
|
+
def compose(self):
|
26
|
+
"""Compose the data table layout."""
|
27
|
+
self._table = DataTable(id="enhanced-data-table")
|
28
|
+
self._table.cursor_type = "row"
|
29
|
+
yield self._table
|
30
|
+
|
31
|
+
def clear_table(self) -> bool:
|
32
|
+
"""
|
33
|
+
Clear the table contents safely.
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
True if cleared successfully, False if recreation was needed
|
37
|
+
"""
|
38
|
+
if not self._table:
|
39
|
+
return False
|
40
|
+
|
41
|
+
try:
|
42
|
+
self._table.clear(columns=True)
|
43
|
+
return True
|
44
|
+
except Exception as e:
|
45
|
+
log.warning(f"Failed to clear table, recreating: {e}")
|
46
|
+
return self._recreate_table()
|
47
|
+
|
48
|
+
def _recreate_table(self) -> bool:
|
49
|
+
"""
|
50
|
+
Recreate the table if clearing failed.
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
True if recreation was successful, False otherwise
|
54
|
+
"""
|
55
|
+
try:
|
56
|
+
if self._table:
|
57
|
+
self._table.remove()
|
58
|
+
|
59
|
+
self._table = DataTable(id="enhanced-data-table")
|
60
|
+
self._table.cursor_type = "row"
|
61
|
+
self.mount(self._table)
|
62
|
+
return True
|
63
|
+
except Exception as e:
|
64
|
+
log.error(f"Failed to recreate table: {e}")
|
65
|
+
return False
|
66
|
+
|
67
|
+
def load_dataframe(self, df: pd.DataFrame, max_rows: Optional[int] = None) -> bool:
|
68
|
+
"""
|
69
|
+
Load a pandas DataFrame into the table.
|
70
|
+
|
71
|
+
Args:
|
72
|
+
df: The DataFrame to load
|
73
|
+
max_rows: Optional maximum number of rows to display
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
True if loaded successfully, False otherwise
|
77
|
+
"""
|
78
|
+
if not self._table:
|
79
|
+
log.error("Table not initialized")
|
80
|
+
return False
|
81
|
+
|
82
|
+
try:
|
83
|
+
# Clear existing content
|
84
|
+
if not self.clear_table():
|
85
|
+
return False
|
86
|
+
|
87
|
+
# Handle empty DataFrame
|
88
|
+
if df.empty:
|
89
|
+
self._show_empty_message()
|
90
|
+
return True
|
91
|
+
|
92
|
+
# Limit rows if specified
|
93
|
+
display_df = df.head(max_rows) if max_rows else df
|
94
|
+
|
95
|
+
# Add columns
|
96
|
+
columns = [str(col) for col in display_df.columns]
|
97
|
+
self._table.add_columns(*columns)
|
98
|
+
|
99
|
+
# Add rows
|
100
|
+
rows_data = self._prepare_rows_data(display_df)
|
101
|
+
self._table.add_rows(rows_data)
|
102
|
+
|
103
|
+
log.info(f"Loaded {len(display_df)} rows and {len(columns)} columns into table")
|
104
|
+
return True
|
105
|
+
|
106
|
+
except Exception as e:
|
107
|
+
log.exception(f"Error loading DataFrame into table: {e}")
|
108
|
+
self._show_error_message(f"Failed to load data: {e}")
|
109
|
+
return False
|
110
|
+
|
111
|
+
def _prepare_rows_data(self, df: pd.DataFrame) -> List[Tuple[str, ...]]:
|
112
|
+
"""
|
113
|
+
Prepare DataFrame rows for the DataTable.
|
114
|
+
|
115
|
+
Args:
|
116
|
+
df: The DataFrame to process
|
117
|
+
|
118
|
+
Returns:
|
119
|
+
List of tuples representing table rows
|
120
|
+
"""
|
121
|
+
rows_data = []
|
122
|
+
for row in df.itertuples(index=False, name=None):
|
123
|
+
# Convert each item to string, handling NaN values
|
124
|
+
row_strings = tuple(
|
125
|
+
str(item) if pd.notna(item) else ""
|
126
|
+
for item in row
|
127
|
+
)
|
128
|
+
rows_data.append(row_strings)
|
129
|
+
return rows_data
|
130
|
+
|
131
|
+
def _show_empty_message(self) -> None:
|
132
|
+
"""Show a message when the DataFrame is empty."""
|
133
|
+
try:
|
134
|
+
self.query("Static").remove() # Remove any existing messages
|
135
|
+
empty_msg = Static("No data available in the selected range or file is empty.",
|
136
|
+
classes="info-content")
|
137
|
+
self.mount(empty_msg)
|
138
|
+
except Exception as e:
|
139
|
+
log.error(f"Failed to show empty message: {e}")
|
140
|
+
|
141
|
+
def _show_error_message(self, message: str) -> None:
|
142
|
+
"""Show an error message in the table area."""
|
143
|
+
try:
|
144
|
+
self.query("DataTable, Static").remove() # Remove table and any messages
|
145
|
+
error_msg = Static(f"[red]{message}[/red]", classes="error-content")
|
146
|
+
self.mount(error_msg)
|
147
|
+
except Exception as e:
|
148
|
+
log.error(f"Failed to show error message: {e}")
|
149
|
+
|
150
|
+
def get_table(self) -> Optional[DataTable]:
|
151
|
+
"""Get the underlying DataTable widget."""
|
152
|
+
return self._table
|