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.
Files changed (34) hide show
  1. parqv/__init__.py +31 -0
  2. parqv/app.py +84 -102
  3. parqv/cli.py +112 -0
  4. parqv/core/__init__.py +31 -0
  5. parqv/core/config.py +25 -0
  6. parqv/core/file_utils.py +88 -0
  7. parqv/core/handler_factory.py +89 -0
  8. parqv/core/logging.py +46 -0
  9. parqv/data_sources/__init__.py +44 -0
  10. parqv/data_sources/base/__init__.py +28 -0
  11. parqv/data_sources/base/exceptions.py +38 -0
  12. parqv/{handlers/base_handler.py → data_sources/base/handler.py} +54 -25
  13. parqv/{handlers → data_sources/formats}/__init__.py +8 -5
  14. parqv/{handlers → data_sources/formats}/json.py +31 -32
  15. parqv/{handlers → data_sources/formats}/parquet.py +40 -56
  16. parqv/views/__init__.py +38 -0
  17. parqv/views/base.py +98 -0
  18. parqv/views/components/__init__.py +13 -0
  19. parqv/views/components/enhanced_data_table.py +152 -0
  20. parqv/views/components/error_display.py +72 -0
  21. parqv/views/components/loading_display.py +44 -0
  22. parqv/views/data_view.py +119 -46
  23. parqv/views/metadata_view.py +57 -20
  24. parqv/views/schema_view.py +190 -200
  25. parqv/views/utils/__init__.py +13 -0
  26. parqv/views/utils/data_formatters.py +162 -0
  27. parqv/views/utils/stats_formatters.py +160 -0
  28. {parqv-0.2.0.dist-info → parqv-0.2.1.dist-info}/METADATA +2 -2
  29. parqv-0.2.1.dist-info/RECORD +34 -0
  30. {parqv-0.2.0.dist-info → parqv-0.2.1.dist-info}/WHEEL +1 -1
  31. parqv-0.2.0.dist-info/RECORD +0 -17
  32. {parqv-0.2.0.dist-info → parqv-0.2.1.dist-info}/entry_points.txt +0 -0
  33. {parqv-0.2.0.dist-info → parqv-0.2.1.dist-info}/licenses/LICENSE +0 -0
  34. {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 .base_handler import DataHandler, DataHandlerError
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
- log.info(f"Successfully initialized ParquetHandler for: {self.file_path.name}")
49
+ self.logger.info(f"Successfully initialized ParquetHandler for: {self.file_path.name}")
53
50
 
54
51
  except FileNotFoundError as fnf_e:
55
- log.error(f"File not found during ParquetHandler initialization: {fnf_e}")
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
- log.error(f"Arrow IO Error initializing ParquetHandler for {self.file_path.name}: {arrow_io_e}")
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
- log.exception(f"Unexpected error initializing ParquetHandler for {self.file_path.name}")
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
- log.info(f"Closed Parquet file: {self.file_path.name}")
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
- log.warning(f"Exception while closing Parquet file {self.file_path.name}: {e}")
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
- log.warning(f"Metadata or schema not available for summary: {self.file_path.name}")
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
- log.exception(f"Error generating metadata summary for {self.file_path.name}")
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
- log.warning(f"Schema is not available for get_schema_data: {self.file_path.name}")
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
- log.error(f"Error processing field '{field.name}' for schema data: {e}", exc_info=True)
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
- log.warning(f"ParquetFile handler not available for data preview: {self.file_path.name}")
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
- log.info(f"Parquet file is empty based on metadata: {self.file_path.name}")
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
- log.warning(
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
- log.info(f"No data read for preview (file likely empty): {self.file_path.name}")
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
- log.info(f"Generated preview of {len(df)} rows for {self.file_path.name}")
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
- log.exception(f"Error generating data preview from Parquet file: {self.file_path.name}")
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
- log.warning(f"Parquet file/schema unavailable for column stats: {self.file_path.name}")
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
- log.warning(f"Column '{column_name}' not found in schema: {self.file_path.name}")
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
- log.debug(
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
- log.info(f"Column '{column_name}' read resulted in 0 rows.")
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
- log.debug(f"Calculating stats for type: {self._format_pyarrow_type(col_type)}")
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
- log.warning(f"Statistics calculation not fully implemented for type: {col_type}")
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
- log.exception(f"Error during type-specific calculation for column '{column_name}': {calc_err}")
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
- log.exception(f"Arrow error during stats processing for column '{column_name}': {arrow_e}")
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
- log.exception(f"Unexpected error during stats calculation for column '{column_name}'")
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
- log.warning(f"Could not decode metadata bytes: {e}. Value: {value!r}")
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
- log.warning(f"Could not decode key-value metadata: {e}")
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 and optionally length stats for string/binary."""
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
- log.warning(f"Boolean value count calculation error: {vc_e}", exc_info=True)
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
- log.debug(f"Calculating dictionary stats based on value type: {value_type}")
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
- log.warning(f"Arrow error decoding dictionary type for stats: {arrow_decode_err}")
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
- log.warning(f"Could not process dictionary type for stats: {dict_e}")
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
- log.warning(f"Column index {col_index} out of bounds for row group {i}.")
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
- log.warning(f"Error processing metadata stats for RG {i}, column '{column_name}': {e}")
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
- log.warning(f"Column '{column_name}' not found in schema for metadata stats.")
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
- log.exception(f"Failed to get metadata statistics structure for column '{column_name}'.")
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
- log.warning(f"Column index {col_index} out of bounds for row group {rg_meta.num_columns} columns.")
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
- log.error(f"Error reading column chunk metadata stats for index {col_index}: {e}", exc_info=True)
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
- log.error(f"Error formatting type for column {column_name}: {e}")
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