parqv 0.2.0__py3-none-any.whl → 0.3.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.
Files changed (36) 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 +26 -0
  6. parqv/core/file_utils.py +88 -0
  7. parqv/core/handler_factory.py +90 -0
  8. parqv/core/logging.py +46 -0
  9. parqv/data_sources/__init__.py +48 -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 +13 -5
  14. parqv/data_sources/formats/csv.py +460 -0
  15. parqv/{handlers → data_sources/formats}/json.py +68 -32
  16. parqv/{handlers → data_sources/formats}/parquet.py +67 -56
  17. parqv/views/__init__.py +38 -0
  18. parqv/views/base.py +98 -0
  19. parqv/views/components/__init__.py +13 -0
  20. parqv/views/components/enhanced_data_table.py +152 -0
  21. parqv/views/components/error_display.py +72 -0
  22. parqv/views/components/loading_display.py +44 -0
  23. parqv/views/data_view.py +119 -46
  24. parqv/views/metadata_view.py +57 -20
  25. parqv/views/schema_view.py +190 -200
  26. parqv/views/utils/__init__.py +19 -0
  27. parqv/views/utils/data_formatters.py +184 -0
  28. parqv/views/utils/stats_formatters.py +220 -0
  29. parqv/views/utils/visualization.py +204 -0
  30. {parqv-0.2.0.dist-info → parqv-0.3.0.dist-info}/METADATA +5 -6
  31. parqv-0.3.0.dist-info/RECORD +36 -0
  32. {parqv-0.2.0.dist-info → parqv-0.3.0.dist-info}/WHEEL +1 -1
  33. parqv-0.2.0.dist-info/RECORD +0 -17
  34. {parqv-0.2.0.dist-info → parqv-0.3.0.dist-info}/entry_points.txt +0 -0
  35. {parqv-0.2.0.dist-info → parqv-0.3.0.dist-info}/licenses/LICENSE +0 -0
  36. {parqv-0.2.0.dist-info → parqv-0.3.0.dist-info}/top_level.txt +0 -0
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
@@ -0,0 +1,72 @@
1
+ """
2
+ Error display component for parqv views.
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ from textual.containers import VerticalScroll
8
+ from textual.widgets import Static, Label
9
+
10
+
11
+ class ErrorDisplay(VerticalScroll):
12
+ """
13
+ A reusable component for displaying error messages in a consistent format.
14
+ """
15
+
16
+ def __init__(self,
17
+ title: str = "Error",
18
+ message: str = "An error occurred",
19
+ details: Optional[str] = None,
20
+ **kwargs):
21
+ """
22
+ Initialize the error display.
23
+
24
+ Args:
25
+ title: Error title/category
26
+ message: Main error message
27
+ details: Optional detailed error information
28
+ **kwargs: Additional arguments for VerticalScroll
29
+ """
30
+ super().__init__(**kwargs)
31
+ self.title = title
32
+ self.message = message
33
+ self.details = details
34
+
35
+ def compose(self):
36
+ """Compose the error display layout."""
37
+ yield Label(self.title, classes="error-title")
38
+ yield Static(f"[red]{self.message}[/red]", classes="error-content")
39
+
40
+ if self.details:
41
+ yield Static("Details:", classes="error-details-label")
42
+ yield Static(f"[dim]{self.details}[/dim]", classes="error-details")
43
+
44
+ @classmethod
45
+ def file_not_found(cls, file_path: str, **kwargs) -> 'ErrorDisplay':
46
+ """Create an error display for file not found errors."""
47
+ return cls(
48
+ title="File Not Found",
49
+ message=f"Could not find file: {file_path}",
50
+ details="Please check that the file path is correct and the file exists.",
51
+ **kwargs
52
+ )
53
+
54
+ @classmethod
55
+ def handler_not_available(cls, **kwargs) -> 'ErrorDisplay':
56
+ """Create an error display for missing data handler."""
57
+ return cls(
58
+ title="Data Handler Not Available",
59
+ message="No data handler is currently loaded",
60
+ details="This usually means the file could not be processed or loaded.",
61
+ **kwargs
62
+ )
63
+
64
+ @classmethod
65
+ def data_loading_error(cls, error_msg: str, **kwargs) -> 'ErrorDisplay':
66
+ """Create an error display for data loading errors."""
67
+ return cls(
68
+ title="Data Loading Error",
69
+ message="Failed to load data from the file",
70
+ details=f"Technical details: {error_msg}",
71
+ **kwargs
72
+ )
@@ -0,0 +1,44 @@
1
+ """
2
+ Loading display component for parqv views.
3
+ """
4
+
5
+ from textual.containers import Center, Middle
6
+ from textual.widgets import LoadingIndicator, Label
7
+
8
+
9
+ class LoadingDisplay(Center):
10
+ """
11
+ A reusable component for displaying loading states in a consistent format.
12
+ """
13
+
14
+ def __init__(self, message: str = "Loading...", **kwargs):
15
+ """
16
+ Initialize the loading display.
17
+
18
+ Args:
19
+ message: Loading message to display
20
+ **kwargs: Additional arguments for Center container
21
+ """
22
+ super().__init__(**kwargs)
23
+ self.message = message
24
+
25
+ def compose(self):
26
+ """Compose the loading display layout."""
27
+ with Middle():
28
+ yield LoadingIndicator()
29
+ yield Label(self.message, classes="loading-message")
30
+
31
+ @classmethod
32
+ def data_loading(cls, **kwargs) -> 'LoadingDisplay':
33
+ """Create a loading display for data loading operations."""
34
+ return cls(message="Loading data...", **kwargs)
35
+
36
+ @classmethod
37
+ def metadata_loading(cls, **kwargs) -> 'LoadingDisplay':
38
+ """Create a loading display for metadata loading operations."""
39
+ return cls(message="Loading metadata...", **kwargs)
40
+
41
+ @classmethod
42
+ def schema_loading(cls, **kwargs) -> 'LoadingDisplay':
43
+ """Create a loading display for schema loading operations."""
44
+ return cls(message="Loading schema...", **kwargs)
parqv/views/data_view.py CHANGED
@@ -1,68 +1,141 @@
1
- import logging
1
+ """
2
+ Data view for displaying tabular data preview.
3
+ """
4
+
2
5
  from typing import Optional
3
6
 
4
7
  import pandas as pd
5
8
  from textual.app import ComposeResult
6
- from textual.containers import Container
7
- from textual.widgets import DataTable, Static
8
9
 
9
- log = logging.getLogger(__name__)
10
+ from .base import BaseView
11
+ from .components import EnhancedDataTable
12
+ from ..core import DEFAULT_PREVIEW_ROWS
13
+
10
14
 
15
+ class DataView(BaseView):
16
+ """
17
+ View for displaying a preview of the data in tabular format.
18
+
19
+ Shows the first N rows of data in an interactive table format
20
+ with proper error handling and loading states.
21
+ """
11
22
 
12
- class DataView(Container):
13
- DEFAULT_ROWS = 50
23
+ def __init__(self, preview_rows: int = DEFAULT_PREVIEW_ROWS, **kwargs):
24
+ """
25
+ Initialize the data view.
26
+
27
+ Args:
28
+ preview_rows: Number of rows to show in preview
29
+ **kwargs: Additional arguments for BaseView
30
+ """
31
+ super().__init__(**kwargs)
32
+ self.preview_rows = preview_rows
33
+ self._data_table: Optional[EnhancedDataTable] = None
14
34
 
15
35
  def compose(self) -> ComposeResult:
16
- yield DataTable(id="data-table")
36
+ """Compose the data view layout."""
37
+ self._data_table = EnhancedDataTable(id="data-preview-table")
38
+ yield self._data_table
17
39
 
18
- def on_mount(self) -> None:
19
- self.load_data()
40
+ def load_content(self) -> None:
41
+ """Load and display data content."""
42
+ if not self.check_handler_available():
43
+ return
20
44
 
21
- def load_data(self):
22
- table: Optional[DataTable] = self.query_one("#data-table", DataTable)
45
+ if not self._data_table:
46
+ self.show_error("Data table component not initialized")
47
+ return
23
48
 
24
49
  try:
25
- table.clear(columns=True)
26
- except Exception as e:
27
- log.error(f"Error clearing DataTable: {e}")
28
- try:
29
- table.remove()
30
- table = DataTable(id="data-table")
31
- self.mount(table)
32
- except Exception as remount_e:
33
- log.error(f"Failed to remount DataTable: {remount_e}")
34
- self.mount(Static("[red]Error initializing data table.[/red]", classes="error-content"))
50
+ # Get data preview from handler
51
+ self.logger.info(f"Loading data preview ({self.preview_rows} rows)")
52
+ df = self.handler.get_data_preview(num_rows=self.preview_rows)
53
+
54
+ # Validate DataFrame
55
+ if df is None:
56
+ self.show_error("Could not load data preview - handler returned None")
35
57
  return
36
58
 
37
- try:
38
- if not self.app.handler:
39
- self.mount(Static("Parquet handler not available.", classes="error-content"))
59
+ # Handle error DataFrame (some handlers return error as DataFrame)
60
+ if self._is_error_dataframe(df):
61
+ error_msg = self._extract_error_from_dataframe(df)
62
+ self.show_error(error_msg)
40
63
  return
41
64
 
42
- df: Optional[pd.DataFrame] = self.app.handler.get_data_preview(num_rows=self.DEFAULT_ROWS)
65
+ # Load DataFrame into table
66
+ success = self._data_table.load_dataframe(df, max_rows=self.preview_rows)
43
67
 
44
- if df is None:
45
- self.mount(Static("Could not load data preview."))
46
- return
68
+ if success:
69
+ self.logger.info(f"Data preview loaded successfully: {len(df)} rows")
70
+ else:
71
+ self.show_error("Failed to load data into table component")
47
72
 
48
- if df.empty:
49
- self.mount(Static("No data in the preview range or file is empty."))
50
- return
73
+ except Exception as e:
74
+ self.show_error("Failed to load data preview", e)
75
+
76
+ def _is_error_dataframe(self, df: pd.DataFrame) -> bool:
77
+ """
78
+ Check if the DataFrame represents an error condition.
79
+
80
+ Args:
81
+ df: DataFrame to check
82
+
83
+ Returns:
84
+ True if the DataFrame contains error information
85
+ """
86
+ return (
87
+ not df.empty and
88
+ "error" in df.columns and
89
+ len(df.columns) == 1
90
+ )
91
+
92
+ def _extract_error_from_dataframe(self, df: pd.DataFrame) -> str:
93
+ """
94
+ Extract error message from an error DataFrame.
95
+
96
+ Args:
97
+ df: Error DataFrame
98
+
99
+ Returns:
100
+ Error message string
101
+ """
102
+ try:
103
+ if not df.empty and "error" in df.columns:
104
+ return str(df["error"].iloc[0])
105
+ except Exception:
106
+ pass
107
+ return "Unknown error in data loading"
108
+
109
+ def refresh_data(self) -> None:
110
+ """Refresh the data display."""
111
+ self.clear_content()
112
+ self.load_content()
51
113
 
52
- table.cursor_type = "row"
53
- columns = [str(col) for col in df.columns]
54
- table.add_columns(*columns)
55
- rows_data = [
56
- tuple(str(item) if pd.notna(item) else "" for item in row)
57
- for row in df.itertuples(index=False, name=None)
58
- ]
59
- table.add_rows(rows_data)
60
- log.info("DataTable populated successfully.")
114
+ def set_preview_rows(self, new_rows: int) -> None:
115
+ """
116
+ Update the number of preview rows and refresh display.
117
+
118
+ Args:
119
+ new_rows: New number of rows to preview
120
+ """
121
+ if new_rows > 0:
122
+ self.preview_rows = new_rows
123
+ self.refresh_data()
124
+ else:
125
+ self.logger.warning(f"Invalid preview_rows value: {new_rows}")
61
126
 
127
+ def get_current_data(self) -> Optional[pd.DataFrame]:
128
+ """
129
+ Get the currently displayed data if available.
130
+
131
+ Returns:
132
+ Currently loaded DataFrame or None
133
+ """
134
+ if not self.handler:
135
+ return None
136
+
137
+ try:
138
+ return self.handler.get_data_preview(num_rows=self.preview_rows)
62
139
  except Exception as e:
63
- log.exception("Error loading data preview in DataView:")
64
- try:
65
- self.query("DataTable, Static").remove()
66
- self.mount(Static(f"Error loading data preview: {e}", classes="error-content"))
67
- except Exception as display_e:
68
- log.error(f"Error displaying error message: {display_e}")
140
+ self.logger.error(f"Failed to get current data: {e}")
141
+ return None
@@ -1,26 +1,63 @@
1
- import logging
2
- from textual.containers import VerticalScroll
3
- from textual.widgets import Static, Pretty
1
+ """
2
+ Metadata view for displaying file metadata information.
3
+ """
4
4
 
5
- log = logging.getLogger(__name__)
5
+ from textual.containers import VerticalScroll
6
+ from textual.widgets import Pretty
6
7
 
7
- class MetadataView(VerticalScroll):
8
+ from .base import BaseView
9
+ from .components import ErrorDisplay
10
+ from .utils import format_metadata_for_display
8
11
 
9
- def on_mount(self) -> None:
10
- self.load_metadata()
11
12
 
12
- def load_metadata(self):
13
- self.query("*").remove()
13
+ class MetadataView(BaseView):
14
+ """
15
+ View for displaying metadata information about the loaded file.
16
+
17
+ Shows file statistics, format information, and other metadata
18
+ in a formatted display.
19
+ """
20
+
21
+ def load_content(self) -> None:
22
+ """Load and display metadata content."""
23
+ if not self.check_handler_available():
24
+ return
25
+
26
+ try:
27
+ # Get raw metadata from handler
28
+ raw_metadata = self.handler.get_metadata_summary()
29
+
30
+ # Format metadata for display
31
+ formatted_metadata = format_metadata_for_display(raw_metadata)
32
+
33
+ # Check if there's an error in the formatted data
34
+ if "Error" in formatted_metadata and len(formatted_metadata) == 1:
35
+ self.show_error(formatted_metadata["Error"])
36
+ return
37
+
38
+ # Display the formatted metadata
39
+ self._display_metadata(formatted_metadata)
40
+
41
+ self.logger.info("Metadata loaded successfully")
42
+
43
+ except Exception as e:
44
+ self.show_error("Failed to load metadata", e)
45
+
46
+ def _display_metadata(self, metadata: dict) -> None:
47
+ """
48
+ Display the formatted metadata using Pretty widget.
49
+
50
+ Args:
51
+ metadata: Formatted metadata dictionary
52
+ """
14
53
  try:
15
- if self.app.handler:
16
- meta_data = self.app.handler.get_metadata_summary()
17
- if meta_data.get("error"):
18
- self.mount(Static(f"[red]Error getting metadata: {meta_data['error']}[/red]", classes="error-content"))
19
- else:
20
- pretty_widget = Pretty(meta_data)
21
- self.mount(pretty_widget)
22
- else:
23
- self.mount(Static("[red]Data handler not available.[/red]", classes="error-content"))
54
+ pretty_widget = Pretty(metadata, id="metadata-pretty")
55
+ self.mount(pretty_widget)
24
56
  except Exception as e:
25
- log.exception("Error loading metadata view")
26
- self.mount(Static(f"[red]Error loading metadata: {e}[/red]", classes="error-content"))
57
+ self.logger.error(f"Failed to create Pretty widget: {e}")
58
+ self.show_error("Failed to display metadata")
59
+
60
+ def refresh_metadata(self) -> None:
61
+ """Refresh the metadata display."""
62
+ self.clear_content()
63
+ self.load_content()