gitflow-analytics 1.0.3__py3-none-any.whl → 1.3.6__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 (116) hide show
  1. gitflow_analytics/_version.py +1 -1
  2. gitflow_analytics/classification/__init__.py +31 -0
  3. gitflow_analytics/classification/batch_classifier.py +752 -0
  4. gitflow_analytics/classification/classifier.py +464 -0
  5. gitflow_analytics/classification/feature_extractor.py +725 -0
  6. gitflow_analytics/classification/linguist_analyzer.py +574 -0
  7. gitflow_analytics/classification/model.py +455 -0
  8. gitflow_analytics/cli.py +4108 -350
  9. gitflow_analytics/cli_rich.py +198 -48
  10. gitflow_analytics/config/__init__.py +43 -0
  11. gitflow_analytics/config/errors.py +261 -0
  12. gitflow_analytics/config/loader.py +904 -0
  13. gitflow_analytics/config/profiles.py +264 -0
  14. gitflow_analytics/config/repository.py +124 -0
  15. gitflow_analytics/config/schema.py +441 -0
  16. gitflow_analytics/config/validator.py +154 -0
  17. gitflow_analytics/config.py +44 -508
  18. gitflow_analytics/core/analyzer.py +1209 -98
  19. gitflow_analytics/core/cache.py +1337 -29
  20. gitflow_analytics/core/data_fetcher.py +1193 -0
  21. gitflow_analytics/core/identity.py +363 -14
  22. gitflow_analytics/core/metrics_storage.py +526 -0
  23. gitflow_analytics/core/progress.py +372 -0
  24. gitflow_analytics/core/schema_version.py +269 -0
  25. gitflow_analytics/extractors/ml_tickets.py +1100 -0
  26. gitflow_analytics/extractors/story_points.py +8 -1
  27. gitflow_analytics/extractors/tickets.py +749 -11
  28. gitflow_analytics/identity_llm/__init__.py +6 -0
  29. gitflow_analytics/identity_llm/analysis_pass.py +231 -0
  30. gitflow_analytics/identity_llm/analyzer.py +464 -0
  31. gitflow_analytics/identity_llm/models.py +76 -0
  32. gitflow_analytics/integrations/github_integration.py +175 -11
  33. gitflow_analytics/integrations/jira_integration.py +461 -24
  34. gitflow_analytics/integrations/orchestrator.py +124 -1
  35. gitflow_analytics/metrics/activity_scoring.py +322 -0
  36. gitflow_analytics/metrics/branch_health.py +470 -0
  37. gitflow_analytics/metrics/dora.py +379 -20
  38. gitflow_analytics/models/database.py +843 -53
  39. gitflow_analytics/pm_framework/__init__.py +115 -0
  40. gitflow_analytics/pm_framework/adapters/__init__.py +50 -0
  41. gitflow_analytics/pm_framework/adapters/jira_adapter.py +1845 -0
  42. gitflow_analytics/pm_framework/base.py +406 -0
  43. gitflow_analytics/pm_framework/models.py +211 -0
  44. gitflow_analytics/pm_framework/orchestrator.py +652 -0
  45. gitflow_analytics/pm_framework/registry.py +333 -0
  46. gitflow_analytics/qualitative/__init__.py +9 -10
  47. gitflow_analytics/qualitative/chatgpt_analyzer.py +259 -0
  48. gitflow_analytics/qualitative/classifiers/__init__.py +3 -3
  49. gitflow_analytics/qualitative/classifiers/change_type.py +518 -244
  50. gitflow_analytics/qualitative/classifiers/domain_classifier.py +272 -165
  51. gitflow_analytics/qualitative/classifiers/intent_analyzer.py +321 -222
  52. gitflow_analytics/qualitative/classifiers/llm/__init__.py +35 -0
  53. gitflow_analytics/qualitative/classifiers/llm/base.py +193 -0
  54. gitflow_analytics/qualitative/classifiers/llm/batch_processor.py +383 -0
  55. gitflow_analytics/qualitative/classifiers/llm/cache.py +479 -0
  56. gitflow_analytics/qualitative/classifiers/llm/cost_tracker.py +435 -0
  57. gitflow_analytics/qualitative/classifiers/llm/openai_client.py +403 -0
  58. gitflow_analytics/qualitative/classifiers/llm/prompts.py +373 -0
  59. gitflow_analytics/qualitative/classifiers/llm/response_parser.py +287 -0
  60. gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +607 -0
  61. gitflow_analytics/qualitative/classifiers/risk_analyzer.py +215 -189
  62. gitflow_analytics/qualitative/core/__init__.py +4 -4
  63. gitflow_analytics/qualitative/core/llm_fallback.py +239 -235
  64. gitflow_analytics/qualitative/core/nlp_engine.py +157 -148
  65. gitflow_analytics/qualitative/core/pattern_cache.py +214 -192
  66. gitflow_analytics/qualitative/core/processor.py +381 -248
  67. gitflow_analytics/qualitative/enhanced_analyzer.py +2236 -0
  68. gitflow_analytics/qualitative/example_enhanced_usage.py +420 -0
  69. gitflow_analytics/qualitative/models/__init__.py +7 -7
  70. gitflow_analytics/qualitative/models/schemas.py +155 -121
  71. gitflow_analytics/qualitative/utils/__init__.py +4 -4
  72. gitflow_analytics/qualitative/utils/batch_processor.py +136 -123
  73. gitflow_analytics/qualitative/utils/cost_tracker.py +142 -140
  74. gitflow_analytics/qualitative/utils/metrics.py +172 -158
  75. gitflow_analytics/qualitative/utils/text_processing.py +146 -104
  76. gitflow_analytics/reports/__init__.py +100 -0
  77. gitflow_analytics/reports/analytics_writer.py +539 -14
  78. gitflow_analytics/reports/base.py +648 -0
  79. gitflow_analytics/reports/branch_health_writer.py +322 -0
  80. gitflow_analytics/reports/classification_writer.py +924 -0
  81. gitflow_analytics/reports/cli_integration.py +427 -0
  82. gitflow_analytics/reports/csv_writer.py +1676 -212
  83. gitflow_analytics/reports/data_models.py +504 -0
  84. gitflow_analytics/reports/database_report_generator.py +427 -0
  85. gitflow_analytics/reports/example_usage.py +344 -0
  86. gitflow_analytics/reports/factory.py +499 -0
  87. gitflow_analytics/reports/formatters.py +698 -0
  88. gitflow_analytics/reports/html_generator.py +1116 -0
  89. gitflow_analytics/reports/interfaces.py +489 -0
  90. gitflow_analytics/reports/json_exporter.py +2770 -0
  91. gitflow_analytics/reports/narrative_writer.py +2287 -158
  92. gitflow_analytics/reports/story_point_correlation.py +1144 -0
  93. gitflow_analytics/reports/weekly_trends_writer.py +389 -0
  94. gitflow_analytics/training/__init__.py +5 -0
  95. gitflow_analytics/training/model_loader.py +377 -0
  96. gitflow_analytics/training/pipeline.py +550 -0
  97. gitflow_analytics/tui/__init__.py +1 -1
  98. gitflow_analytics/tui/app.py +129 -126
  99. gitflow_analytics/tui/screens/__init__.py +3 -3
  100. gitflow_analytics/tui/screens/analysis_progress_screen.py +188 -179
  101. gitflow_analytics/tui/screens/configuration_screen.py +154 -178
  102. gitflow_analytics/tui/screens/loading_screen.py +100 -110
  103. gitflow_analytics/tui/screens/main_screen.py +89 -72
  104. gitflow_analytics/tui/screens/results_screen.py +305 -281
  105. gitflow_analytics/tui/widgets/__init__.py +2 -2
  106. gitflow_analytics/tui/widgets/data_table.py +67 -69
  107. gitflow_analytics/tui/widgets/export_modal.py +76 -76
  108. gitflow_analytics/tui/widgets/progress_widget.py +41 -46
  109. gitflow_analytics-1.3.6.dist-info/METADATA +1015 -0
  110. gitflow_analytics-1.3.6.dist-info/RECORD +122 -0
  111. gitflow_analytics-1.0.3.dist-info/METADATA +0 -490
  112. gitflow_analytics-1.0.3.dist-info/RECORD +0 -62
  113. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/WHEEL +0 -0
  114. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/entry_points.txt +0 -0
  115. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/licenses/LICENSE +0 -0
  116. {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  """Custom widgets for the GitFlow Analytics TUI."""
2
2
 
3
- from .progress_widget import AnalysisProgressWidget
4
3
  from .data_table import EnhancedDataTable
5
4
  from .export_modal import ExportModal
5
+ from .progress_widget import AnalysisProgressWidget
6
6
 
7
- __all__ = ["AnalysisProgressWidget", "EnhancedDataTable", "ExportModal"]
7
+ __all__ = ["AnalysisProgressWidget", "EnhancedDataTable", "ExportModal"]
@@ -1,26 +1,25 @@
1
1
  """Enhanced data table widget for GitFlow Analytics TUI."""
2
2
 
3
- from typing import Any, Dict, List, Optional, Union
4
3
  from datetime import datetime
4
+ from typing import Any, Optional, Union
5
5
 
6
- from textual.widgets import DataTable
7
6
  from textual.reactive import reactive
8
- from rich.text import Text
7
+ from textual.widgets import DataTable
9
8
 
10
9
 
11
10
  class EnhancedDataTable(DataTable):
12
11
  """
13
12
  Enhanced data table with sorting, filtering, and formatting capabilities.
14
-
13
+
15
14
  WHY: The standard DataTable widget lacks features needed for displaying
16
15
  complex analytics data like sorting, filtering, and intelligent formatting
17
16
  of different data types (dates, numbers, etc.).
18
-
17
+
19
18
  DESIGN DECISION: Extends DataTable rather than creating from scratch to
20
19
  maintain compatibility with Textual's data table features while adding
21
20
  the necessary enhancements for analytics display.
22
21
  """
23
-
22
+
24
23
  DEFAULT_CSS = """
25
24
  EnhancedDataTable {
26
25
  height: auto;
@@ -40,29 +39,29 @@ class EnhancedDataTable(DataTable):
40
39
  background: $primary 30%;
41
40
  }
42
41
  """
43
-
42
+
44
43
  # Reactive attributes for dynamic updates
45
44
  sort_column = reactive("")
46
45
  sort_reverse = reactive(False)
47
46
  filter_text = reactive("")
48
-
47
+
49
48
  def __init__(
50
49
  self,
51
- data: Optional[List[Dict[str, Any]]] = None,
50
+ data: Optional[list[dict[str, Any]]] = None,
52
51
  *,
53
52
  name: Optional[str] = None,
54
53
  id: Optional[str] = None,
55
- classes: Optional[str] = None
54
+ classes: Optional[str] = None,
56
55
  ) -> None:
57
56
  super().__init__(name=name, id=id, classes=classes)
58
57
  self._raw_data = data or []
59
58
  self._filtered_data = []
60
59
  self._column_formatters = {}
61
-
62
- def set_data(self, data: List[Dict[str, Any]]) -> None:
60
+
61
+ def set_data(self, data: list[dict[str, Any]]) -> None:
63
62
  """
64
63
  Set table data with automatic column detection and formatting.
65
-
64
+
66
65
  WHY: Automatically handles different data types and formats them
67
66
  appropriately for display, reducing the need for manual formatting
68
67
  in calling code.
@@ -70,91 +69,95 @@ class EnhancedDataTable(DataTable):
70
69
  self._raw_data = data
71
70
  if not data:
72
71
  return
73
-
72
+
74
73
  # Clear existing data
75
74
  self.clear()
76
-
75
+
77
76
  # Get columns from first row
78
77
  columns = list(data[0].keys())
79
-
78
+
80
79
  # Add columns with appropriate widths
81
80
  for col in columns:
82
81
  width = self._calculate_column_width(col, data)
83
82
  self.add_column(self._format_column_header(col), width=width, key=col)
84
-
83
+
85
84
  # Set up formatters based on data types
86
85
  self._setup_formatters(data)
87
-
86
+
88
87
  # Add data rows
89
88
  self._apply_filter_and_sort()
90
-
91
- def _calculate_column_width(self, column: str, data: List[Dict[str, Any]]) -> int:
89
+
90
+ def _calculate_column_width(self, column: str, data: list[dict[str, Any]]) -> int:
92
91
  """
93
92
  Calculate appropriate column width based on content.
94
-
93
+
95
94
  WHY: Dynamically sizes columns based on actual content to optimize
96
95
  display space while ensuring all content is visible.
97
96
  """
98
97
  # Start with header width
99
98
  max_width = len(self._format_column_header(column))
100
-
99
+
101
100
  # Check sample of data for width
102
101
  sample_size = min(50, len(data))
103
102
  for row in data[:sample_size]:
104
103
  value = row.get(column, "")
105
104
  formatted_value = self._format_cell_value(column, value)
106
105
  max_width = max(max_width, len(str(formatted_value)))
107
-
106
+
108
107
  # Set reasonable bounds
109
108
  return min(max(max_width + 2, 8), 50)
110
-
109
+
111
110
  def _format_column_header(self, column: str) -> str:
112
111
  """Format column header for display."""
113
112
  # Convert snake_case to Title Case
114
- return column.replace('_', ' ').title()
115
-
116
- def _setup_formatters(self, data: List[Dict[str, Any]]) -> None:
113
+ return column.replace("_", " ").title()
114
+
115
+ def _setup_formatters(self, data: list[dict[str, Any]]) -> None:
117
116
  """
118
117
  Set up column formatters based on data types.
119
-
118
+
120
119
  WHY: Automatically detects data types and applies appropriate formatting
121
120
  (dates, numbers, percentages) to improve readability.
122
121
  """
123
122
  if not data:
124
123
  return
125
-
124
+
126
125
  sample_row = data[0]
127
-
126
+
128
127
  for column, value in sample_row.items():
129
128
  if isinstance(value, datetime):
130
129
  self._column_formatters[column] = self._format_datetime
131
130
  elif isinstance(value, (int, float)):
132
131
  # Check if it looks like a percentage
133
- if any('pct' in column.lower() or 'percent' in column.lower()
134
- or 'rate' in column.lower() for _ in [column]):
132
+ if any(
133
+ "pct" in column.lower()
134
+ or "percent" in column.lower()
135
+ or "rate" in column.lower()
136
+ for _ in [column]
137
+ ):
135
138
  self._column_formatters[column] = self._format_percentage
136
139
  else:
137
140
  self._column_formatters[column] = self._format_number
138
141
  elif isinstance(value, str) and len(value) > 50:
139
142
  self._column_formatters[column] = self._format_long_text
140
-
143
+
141
144
  def _format_cell_value(self, column: str, value: Any) -> str:
142
145
  """Format individual cell value."""
143
146
  if value is None:
144
147
  return ""
145
-
148
+
146
149
  formatter = self._column_formatters.get(column, str)
147
150
  return formatter(value)
148
-
151
+
149
152
  def _format_datetime(self, value: datetime) -> str:
150
153
  """Format datetime values."""
151
154
  if isinstance(value, str):
152
155
  try:
153
- value = datetime.fromisoformat(value.replace('Z', '+00:00'))
154
- except:
156
+ value = datetime.fromisoformat(value.replace("Z", "+00:00"))
157
+ except Exception:
155
158
  return str(value)
156
- return value.strftime('%Y-%m-%d %H:%M')
157
-
159
+ return value.strftime("%Y-%m-%d %H:%M")
160
+
158
161
  def _format_number(self, value: Union[int, float]) -> str:
159
162
  """Format numeric values."""
160
163
  if isinstance(value, float):
@@ -163,95 +166,90 @@ class EnhancedDataTable(DataTable):
163
166
  else:
164
167
  return f"{value:.2f}"
165
168
  return f"{value:,}"
166
-
169
+
167
170
  def _format_percentage(self, value: Union[int, float]) -> str:
168
171
  """Format percentage values."""
169
172
  return f"{value:.1f}%"
170
-
173
+
171
174
  def _format_long_text(self, value: str) -> str:
172
175
  """Format long text values."""
173
176
  if len(value) > 47:
174
177
  return value[:44] + "..."
175
178
  return value
176
-
179
+
177
180
  def _apply_filter_and_sort(self) -> None:
178
181
  """
179
182
  Apply current filter and sort settings to data.
180
-
183
+
181
184
  WHY: Provides real-time filtering and sorting capabilities that are
182
185
  essential for exploring large datasets in the analytics results.
183
186
  """
184
187
  # Start with all data
185
188
  filtered_data = self._raw_data.copy()
186
-
189
+
187
190
  # Apply filter if set
188
191
  if self.filter_text:
189
192
  filtered_data = [
190
- row for row in filtered_data
191
- if any(self.filter_text.lower() in str(value).lower()
192
- for value in row.values())
193
+ row
194
+ for row in filtered_data
195
+ if any(self.filter_text.lower() in str(value).lower() for value in row.values())
193
196
  ]
194
-
197
+
195
198
  # Apply sort if set
196
199
  if self.sort_column and self.sort_column in (filtered_data[0] if filtered_data else {}):
197
200
  try:
198
201
  filtered_data.sort(
199
- key=lambda x: x.get(self.sort_column, ""),
200
- reverse=self.sort_reverse
202
+ key=lambda x: x.get(self.sort_column, ""), reverse=self.sort_reverse
201
203
  )
202
204
  except TypeError:
203
205
  # Handle mixed types by converting to string
204
206
  filtered_data.sort(
205
- key=lambda x: str(x.get(self.sort_column, "")),
206
- reverse=self.sort_reverse
207
+ key=lambda x: str(x.get(self.sort_column, "")), reverse=self.sort_reverse
207
208
  )
208
-
209
+
209
210
  self._filtered_data = filtered_data
210
-
211
+
211
212
  # Clear and repopulate table
212
213
  self.clear(columns=False) # Keep columns, clear rows
213
-
214
+
214
215
  for row in filtered_data:
215
- formatted_row = [
216
- self._format_cell_value(col, row.get(col, ""))
217
- for col in row.keys()
218
- ]
216
+ formatted_row = [self._format_cell_value(col, row.get(col, "")) for col in row]
219
217
  self.add_row(*formatted_row)
220
-
218
+
221
219
  def sort_by_column(self, column: str, reverse: bool = False) -> None:
222
220
  """Sort table by specified column."""
223
221
  self.sort_column = column
224
222
  self.sort_reverse = reverse
225
223
  self._apply_filter_and_sort()
226
-
224
+
227
225
  def filter_data(self, filter_text: str) -> None:
228
226
  """Filter table data by text search."""
229
227
  self.filter_text = filter_text
230
228
  self._apply_filter_and_sort()
231
-
229
+
232
230
  def export_to_csv(self, filename: str) -> None:
233
231
  """
234
232
  Export current filtered/sorted data to CSV.
235
-
233
+
236
234
  WHY: Allows users to export the specific view they've filtered and
237
235
  sorted, maintaining their exploration context.
238
236
  """
239
237
  import csv
240
-
238
+
241
239
  if not self._filtered_data:
242
240
  return
243
-
244
- with open(filename, 'w', newline='', encoding='utf-8') as csvfile:
241
+
242
+ with open(filename, "w", newline="", encoding="utf-8") as csvfile:
245
243
  if self._filtered_data:
246
244
  fieldnames = list(self._filtered_data[0].keys())
247
245
  writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
248
246
  writer.writeheader()
249
247
  writer.writerows(self._filtered_data)
250
-
248
+
251
249
  def get_row_count(self) -> int:
252
250
  """Get number of rows currently displayed."""
253
251
  return len(self._filtered_data)
254
-
252
+
255
253
  def get_total_count(self) -> int:
256
254
  """Get total number of rows in dataset."""
257
- return len(self._raw_data)
255
+ return len(self._raw_data)
@@ -1,28 +1,28 @@
1
1
  """Export modal dialog for GitFlow Analytics TUI."""
2
2
 
3
3
  from pathlib import Path
4
- from typing import Optional, Dict, Any, List, Callable
4
+ from typing import Any, Optional
5
5
 
6
- from textual.widgets import Button, Input, Label, Select, Switch, Static
7
- from textual.containers import Container, Horizontal, Vertical
8
- from textual.screen import ModalScreen
9
6
  from textual.binding import Binding
7
+ from textual.containers import Container, Horizontal
10
8
  from textual.message import Message
9
+ from textual.screen import ModalScreen
10
+ from textual.widgets import Button, Input, Label, Select, Static, Switch
11
11
 
12
12
 
13
- class ExportModal(ModalScreen[Optional[Dict[str, Any]]]):
13
+ class ExportModal(ModalScreen[Optional[dict[str, Any]]]):
14
14
  """
15
15
  Modal dialog for configuring and executing data exports.
16
-
16
+
17
17
  WHY: Provides a comprehensive export interface that allows users to
18
18
  choose format, location, and export options without cluttering the
19
19
  main interface. Modal design ensures focused interaction.
20
-
20
+
21
21
  DESIGN DECISION: Returns export configuration as a dictionary rather
22
22
  than executing export directly, allowing the calling code to handle
23
23
  the actual export operation with proper error handling and progress feedback.
24
24
  """
25
-
25
+
26
26
  DEFAULT_CSS = """
27
27
  ExportModal {
28
28
  align: center middle;
@@ -65,40 +65,40 @@ class ExportModal(ModalScreen[Optional[Dict[str, Any]]]):
65
65
  padding: 1 0;
66
66
  }
67
67
  """
68
-
68
+
69
69
  BINDINGS = [
70
70
  Binding("escape", "cancel", "Cancel"),
71
71
  Binding("ctrl+s", "export", "Export"),
72
72
  ]
73
-
73
+
74
74
  class ExportRequested(Message):
75
75
  """Message sent when export is requested."""
76
-
77
- def __init__(self, config: Dict[str, Any]) -> None:
76
+
77
+ def __init__(self, config: dict[str, Any]) -> None:
78
78
  super().__init__()
79
79
  self.config = config
80
-
80
+
81
81
  def __init__(
82
82
  self,
83
- available_formats: Optional[List[str]] = None,
83
+ available_formats: Optional[list[str]] = None,
84
84
  default_path: Optional[Path] = None,
85
- data_info: Optional[Dict[str, Any]] = None
85
+ data_info: Optional[dict[str, Any]] = None,
86
86
  ) -> None:
87
87
  super().__init__()
88
88
  self.available_formats = available_formats or ["CSV", "JSON", "Markdown"]
89
89
  self.default_path = default_path or Path("./reports")
90
90
  self.data_info = data_info or {}
91
-
91
+
92
92
  def compose(self):
93
93
  """Compose the export modal dialog."""
94
94
  with Container(id="export-dialog"):
95
95
  yield Label("Export Data", classes="modal-title")
96
-
96
+
97
97
  # Format selection
98
98
  yield Label("Export Format:", classes="section-title")
99
99
  format_options = [(fmt, fmt.lower()) for fmt in self.available_formats]
100
100
  yield Select(format_options, value=format_options[0][1], id="format-select")
101
-
101
+
102
102
  # File path
103
103
  yield Label("Export Location:", classes="section-title")
104
104
  with Horizontal(classes="form-row"):
@@ -106,86 +106,86 @@ class ExportModal(ModalScreen[Optional[Dict[str, Any]]]):
106
106
  yield Input(
107
107
  value=str(self.default_path),
108
108
  placeholder="Path to export directory",
109
- id="path-input"
109
+ id="path-input",
110
110
  )
111
-
111
+
112
112
  with Horizontal(classes="form-row"):
113
113
  yield Label("Filename:", classes="form-label")
114
114
  yield Input(
115
115
  value=self._generate_default_filename(),
116
116
  placeholder="Export filename",
117
- id="filename-input"
117
+ id="filename-input",
118
118
  )
119
-
119
+
120
120
  # Export options
121
121
  yield Label("Export Options:", classes="section-title")
122
-
122
+
123
123
  with Horizontal(classes="form-row"):
124
124
  yield Label("Include headers:", classes="form-label")
125
125
  yield Switch(value=True, id="include-headers")
126
-
126
+
127
127
  with Horizontal(classes="form-row"):
128
128
  yield Label("Anonymize data:", classes="form-label")
129
129
  yield Switch(value=False, id="anonymize-data")
130
-
130
+
131
131
  # Data info
132
132
  if self.data_info:
133
133
  yield Label("Data Summary:", classes="section-title")
134
134
  info_text = self._format_data_info()
135
135
  yield Static(info_text, id="data-info")
136
-
136
+
137
137
  # Button bar
138
138
  with Horizontal(classes="button-bar"):
139
139
  yield Button("Cancel", variant="default", id="cancel-btn")
140
140
  yield Button("Export", variant="primary", id="export-btn")
141
-
141
+
142
142
  def _generate_default_filename(self) -> str:
143
143
  """
144
144
  Generate default filename based on export format and current date.
145
-
145
+
146
146
  WHY: Provides sensible defaults to reduce user input while ensuring
147
147
  unique filenames that won't accidentally overwrite existing files.
148
148
  """
149
149
  from datetime import datetime
150
-
150
+
151
151
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
152
- data_type = self.data_info.get('type', 'export')
152
+ data_type = self.data_info.get("type", "export")
153
153
  return f"{data_type}_{timestamp}"
154
-
154
+
155
155
  def _format_data_info(self) -> str:
156
156
  """Format data information for display."""
157
157
  info_lines = []
158
-
159
- if 'row_count' in self.data_info:
158
+
159
+ if "row_count" in self.data_info:
160
160
  info_lines.append(f"• Rows: {self.data_info['row_count']:,}")
161
-
162
- if 'column_count' in self.data_info:
161
+
162
+ if "column_count" in self.data_info:
163
163
  info_lines.append(f"• Columns: {self.data_info['column_count']}")
164
-
165
- if 'date_range' in self.data_info:
164
+
165
+ if "date_range" in self.data_info:
166
166
  info_lines.append(f"• Date range: {self.data_info['date_range']}")
167
-
168
- if 'data_types' in self.data_info:
169
- types_str = ", ".join(self.data_info['data_types'])
167
+
168
+ if "data_types" in self.data_info:
169
+ types_str = ", ".join(self.data_info["data_types"])
170
170
  info_lines.append(f"• Data types: {types_str}")
171
-
171
+
172
172
  return "\n".join(info_lines)
173
-
173
+
174
174
  def on_button_pressed(self, event: Button.Pressed) -> None:
175
175
  """Handle button press events."""
176
176
  if event.button.id == "cancel-btn":
177
177
  self.action_cancel()
178
178
  elif event.button.id == "export-btn":
179
179
  self.action_export()
180
-
180
+
181
181
  def action_cancel(self) -> None:
182
182
  """Cancel the export operation."""
183
183
  self.dismiss(None)
184
-
184
+
185
185
  def action_export(self) -> None:
186
186
  """
187
187
  Validate inputs and request export operation.
188
-
188
+
189
189
  WHY: Performs comprehensive validation before submitting export
190
190
  request to prevent errors and provide immediate feedback to users.
191
191
  """
@@ -196,57 +196,57 @@ class ExportModal(ModalScreen[Optional[Dict[str, Any]]]):
196
196
  filename_input = self.query_one("#filename-input", Input)
197
197
  include_headers = self.query_one("#include-headers", Switch)
198
198
  anonymize_data = self.query_one("#anonymize-data", Switch)
199
-
199
+
200
200
  # Validate inputs
201
201
  export_path = Path(path_input.value.strip())
202
202
  filename = filename_input.value.strip()
203
-
203
+
204
204
  if not filename:
205
205
  self.notify("Please enter a filename", severity="error")
206
206
  return
207
-
207
+
208
208
  # Add extension if not present
209
209
  selected_format = format_select.value
210
- if selected_format == "csv" and not filename.lower().endswith('.csv'):
211
- filename += '.csv'
212
- elif selected_format == "json" and not filename.lower().endswith('.json'):
213
- filename += '.json'
214
- elif selected_format == "markdown" and not filename.lower().endswith('.md'):
215
- filename += '.md'
216
-
210
+ if selected_format == "csv" and not filename.lower().endswith(".csv"):
211
+ filename += ".csv"
212
+ elif selected_format == "json" and not filename.lower().endswith(".json"):
213
+ filename += ".json"
214
+ elif selected_format == "markdown" and not filename.lower().endswith(".md"):
215
+ filename += ".md"
216
+
217
217
  full_path = export_path / filename
218
-
218
+
219
219
  # Check if file exists
220
220
  if full_path.exists():
221
221
  # In a real implementation, you'd show a confirmation dialog
222
222
  # For now, we'll just proceed with overwrite warning
223
223
  pass
224
-
224
+
225
225
  # Create export configuration
226
226
  export_config = {
227
- 'format': selected_format,
228
- 'path': full_path,
229
- 'include_headers': include_headers.value,
230
- 'anonymize': anonymize_data.value,
227
+ "format": selected_format,
228
+ "path": full_path,
229
+ "include_headers": include_headers.value,
230
+ "anonymize": anonymize_data.value,
231
231
  }
232
-
232
+
233
233
  # Send export request message
234
234
  self.post_message(self.ExportRequested(export_config))
235
235
  self.dismiss(export_config)
236
-
236
+
237
237
  except Exception as e:
238
238
  self.notify(f"Export configuration error: {e}", severity="error")
239
-
239
+
240
240
  def on_select_changed(self, event: Select.Changed) -> None:
241
241
  """Handle format selection changes."""
242
242
  if event.select.id == "format-select":
243
243
  # Update filename extension when format changes
244
244
  filename_input = self.query_one("#filename-input", Input)
245
245
  current_filename = filename_input.value
246
-
246
+
247
247
  # Remove existing extension
248
- name_without_ext = current_filename.rsplit('.', 1)[0]
249
-
248
+ name_without_ext = current_filename.rsplit(".", 1)[0]
249
+
250
250
  # Add new extension
251
251
  new_format = event.value
252
252
  if new_format == "csv":
@@ -257,16 +257,16 @@ class ExportModal(ModalScreen[Optional[Dict[str, Any]]]):
257
257
  new_filename = f"{name_without_ext}.md"
258
258
  else:
259
259
  new_filename = name_without_ext
260
-
260
+
261
261
  filename_input.value = new_filename
262
-
262
+
263
263
  def validate_export_path(self, path: Path) -> tuple[bool, str]:
264
264
  """
265
265
  Validate export path and return validation result.
266
-
266
+
267
267
  WHY: Prevents export failures by validating paths before attempting
268
268
  to write files, providing clear error messages to users.
269
-
269
+
270
270
  @param path: Path to validate
271
271
  @return: Tuple of (is_valid, error_message)
272
272
  """
@@ -280,11 +280,11 @@ class ExportModal(ModalScreen[Optional[Dict[str, Any]]]):
280
280
  return False, f"Permission denied: Cannot create directory {parent_dir}"
281
281
  except Exception as e:
282
282
  return False, f"Cannot create directory {parent_dir}: {e}"
283
-
283
+
284
284
  # Check write permissions
285
285
  if not parent_dir.exists():
286
286
  return False, f"Directory does not exist: {parent_dir}"
287
-
287
+
288
288
  # Try creating a test file to check permissions
289
289
  test_file = parent_dir / f".test_write_{hash(str(path))}"
290
290
  try:
@@ -294,8 +294,8 @@ class ExportModal(ModalScreen[Optional[Dict[str, Any]]]):
294
294
  return False, f"Permission denied: Cannot write to {parent_dir}"
295
295
  except Exception as e:
296
296
  return False, f"Cannot write to {parent_dir}: {e}"
297
-
297
+
298
298
  return True, ""
299
-
299
+
300
300
  except Exception as e:
301
- return False, f"Path validation error: {e}"
301
+ return False, f"Path validation error: {e}"