gitflow-analytics 1.0.1__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.
- gitflow_analytics/__init__.py +11 -11
- gitflow_analytics/_version.py +2 -2
- gitflow_analytics/classification/__init__.py +31 -0
- gitflow_analytics/classification/batch_classifier.py +752 -0
- gitflow_analytics/classification/classifier.py +464 -0
- gitflow_analytics/classification/feature_extractor.py +725 -0
- gitflow_analytics/classification/linguist_analyzer.py +574 -0
- gitflow_analytics/classification/model.py +455 -0
- gitflow_analytics/cli.py +4490 -378
- gitflow_analytics/cli_rich.py +503 -0
- gitflow_analytics/config/__init__.py +43 -0
- gitflow_analytics/config/errors.py +261 -0
- gitflow_analytics/config/loader.py +904 -0
- gitflow_analytics/config/profiles.py +264 -0
- gitflow_analytics/config/repository.py +124 -0
- gitflow_analytics/config/schema.py +441 -0
- gitflow_analytics/config/validator.py +154 -0
- gitflow_analytics/config.py +44 -398
- gitflow_analytics/core/analyzer.py +1320 -172
- gitflow_analytics/core/branch_mapper.py +132 -132
- gitflow_analytics/core/cache.py +1554 -175
- gitflow_analytics/core/data_fetcher.py +1193 -0
- gitflow_analytics/core/identity.py +571 -185
- gitflow_analytics/core/metrics_storage.py +526 -0
- gitflow_analytics/core/progress.py +372 -0
- gitflow_analytics/core/schema_version.py +269 -0
- gitflow_analytics/extractors/base.py +13 -11
- gitflow_analytics/extractors/ml_tickets.py +1100 -0
- gitflow_analytics/extractors/story_points.py +77 -59
- gitflow_analytics/extractors/tickets.py +841 -89
- gitflow_analytics/identity_llm/__init__.py +6 -0
- gitflow_analytics/identity_llm/analysis_pass.py +231 -0
- gitflow_analytics/identity_llm/analyzer.py +464 -0
- gitflow_analytics/identity_llm/models.py +76 -0
- gitflow_analytics/integrations/github_integration.py +258 -87
- gitflow_analytics/integrations/jira_integration.py +572 -123
- gitflow_analytics/integrations/orchestrator.py +206 -82
- gitflow_analytics/metrics/activity_scoring.py +322 -0
- gitflow_analytics/metrics/branch_health.py +470 -0
- gitflow_analytics/metrics/dora.py +542 -179
- gitflow_analytics/models/database.py +986 -59
- gitflow_analytics/pm_framework/__init__.py +115 -0
- gitflow_analytics/pm_framework/adapters/__init__.py +50 -0
- gitflow_analytics/pm_framework/adapters/jira_adapter.py +1845 -0
- gitflow_analytics/pm_framework/base.py +406 -0
- gitflow_analytics/pm_framework/models.py +211 -0
- gitflow_analytics/pm_framework/orchestrator.py +652 -0
- gitflow_analytics/pm_framework/registry.py +333 -0
- gitflow_analytics/qualitative/__init__.py +29 -0
- gitflow_analytics/qualitative/chatgpt_analyzer.py +259 -0
- gitflow_analytics/qualitative/classifiers/__init__.py +13 -0
- gitflow_analytics/qualitative/classifiers/change_type.py +742 -0
- gitflow_analytics/qualitative/classifiers/domain_classifier.py +506 -0
- gitflow_analytics/qualitative/classifiers/intent_analyzer.py +535 -0
- gitflow_analytics/qualitative/classifiers/llm/__init__.py +35 -0
- gitflow_analytics/qualitative/classifiers/llm/base.py +193 -0
- gitflow_analytics/qualitative/classifiers/llm/batch_processor.py +383 -0
- gitflow_analytics/qualitative/classifiers/llm/cache.py +479 -0
- gitflow_analytics/qualitative/classifiers/llm/cost_tracker.py +435 -0
- gitflow_analytics/qualitative/classifiers/llm/openai_client.py +403 -0
- gitflow_analytics/qualitative/classifiers/llm/prompts.py +373 -0
- gitflow_analytics/qualitative/classifiers/llm/response_parser.py +287 -0
- gitflow_analytics/qualitative/classifiers/llm_commit_classifier.py +607 -0
- gitflow_analytics/qualitative/classifiers/risk_analyzer.py +438 -0
- gitflow_analytics/qualitative/core/__init__.py +13 -0
- gitflow_analytics/qualitative/core/llm_fallback.py +657 -0
- gitflow_analytics/qualitative/core/nlp_engine.py +382 -0
- gitflow_analytics/qualitative/core/pattern_cache.py +479 -0
- gitflow_analytics/qualitative/core/processor.py +673 -0
- gitflow_analytics/qualitative/enhanced_analyzer.py +2236 -0
- gitflow_analytics/qualitative/example_enhanced_usage.py +420 -0
- gitflow_analytics/qualitative/models/__init__.py +25 -0
- gitflow_analytics/qualitative/models/schemas.py +306 -0
- gitflow_analytics/qualitative/utils/__init__.py +13 -0
- gitflow_analytics/qualitative/utils/batch_processor.py +339 -0
- gitflow_analytics/qualitative/utils/cost_tracker.py +345 -0
- gitflow_analytics/qualitative/utils/metrics.py +361 -0
- gitflow_analytics/qualitative/utils/text_processing.py +285 -0
- gitflow_analytics/reports/__init__.py +100 -0
- gitflow_analytics/reports/analytics_writer.py +550 -18
- gitflow_analytics/reports/base.py +648 -0
- gitflow_analytics/reports/branch_health_writer.py +322 -0
- gitflow_analytics/reports/classification_writer.py +924 -0
- gitflow_analytics/reports/cli_integration.py +427 -0
- gitflow_analytics/reports/csv_writer.py +1700 -216
- gitflow_analytics/reports/data_models.py +504 -0
- gitflow_analytics/reports/database_report_generator.py +427 -0
- gitflow_analytics/reports/example_usage.py +344 -0
- gitflow_analytics/reports/factory.py +499 -0
- gitflow_analytics/reports/formatters.py +698 -0
- gitflow_analytics/reports/html_generator.py +1116 -0
- gitflow_analytics/reports/interfaces.py +489 -0
- gitflow_analytics/reports/json_exporter.py +2770 -0
- gitflow_analytics/reports/narrative_writer.py +2289 -158
- gitflow_analytics/reports/story_point_correlation.py +1144 -0
- gitflow_analytics/reports/weekly_trends_writer.py +389 -0
- gitflow_analytics/training/__init__.py +5 -0
- gitflow_analytics/training/model_loader.py +377 -0
- gitflow_analytics/training/pipeline.py +550 -0
- gitflow_analytics/tui/__init__.py +5 -0
- gitflow_analytics/tui/app.py +724 -0
- gitflow_analytics/tui/screens/__init__.py +8 -0
- gitflow_analytics/tui/screens/analysis_progress_screen.py +496 -0
- gitflow_analytics/tui/screens/configuration_screen.py +523 -0
- gitflow_analytics/tui/screens/loading_screen.py +348 -0
- gitflow_analytics/tui/screens/main_screen.py +321 -0
- gitflow_analytics/tui/screens/results_screen.py +722 -0
- gitflow_analytics/tui/widgets/__init__.py +7 -0
- gitflow_analytics/tui/widgets/data_table.py +255 -0
- gitflow_analytics/tui/widgets/export_modal.py +301 -0
- gitflow_analytics/tui/widgets/progress_widget.py +187 -0
- gitflow_analytics-1.3.6.dist-info/METADATA +1015 -0
- gitflow_analytics-1.3.6.dist-info/RECORD +122 -0
- gitflow_analytics-1.0.1.dist-info/METADATA +0 -463
- gitflow_analytics-1.0.1.dist-info/RECORD +0 -31
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/WHEEL +0 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.3.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Loading screen for GitFlow Analytics TUI startup."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from textual.binding import Binding
|
|
8
|
+
from textual.containers import Center, Container, Vertical
|
|
9
|
+
from textual.message import Message
|
|
10
|
+
from textual.screen import Screen
|
|
11
|
+
from textual.widgets import Footer, Header, Label, LoadingIndicator, Static
|
|
12
|
+
|
|
13
|
+
from ..widgets.progress_widget import AnalysisProgressWidget
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LoadingScreen(Screen):
|
|
17
|
+
"""
|
|
18
|
+
Loading screen displayed during application startup and heavy initialization.
|
|
19
|
+
|
|
20
|
+
WHY: The TUI application needs to load configurations, spaCy models, and other
|
|
21
|
+
heavy resources during startup. This screen provides user feedback about the
|
|
22
|
+
loading process instead of showing a black screen, improving user experience.
|
|
23
|
+
|
|
24
|
+
DESIGN DECISION: Uses a combination of progress indicators and status messages
|
|
25
|
+
to show both overall progress and specific loading steps. This keeps users
|
|
26
|
+
informed about what's happening and that the application is responsive.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
BINDINGS = [
|
|
30
|
+
Binding("ctrl+c", "cancel", "Cancel Loading"),
|
|
31
|
+
Binding("escape", "cancel", "Cancel Loading"),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
loading_message: str = "Initializing GitFlow Analytics...",
|
|
37
|
+
*,
|
|
38
|
+
name: Optional[str] = None,
|
|
39
|
+
id: Optional[str] = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
super().__init__(name=name, id=id)
|
|
42
|
+
self.loading_message = loading_message
|
|
43
|
+
self.loading_task: Optional[asyncio.Task] = None
|
|
44
|
+
self.start_time = time.time()
|
|
45
|
+
self.cancelled = False
|
|
46
|
+
|
|
47
|
+
def compose(self):
|
|
48
|
+
"""Compose the loading screen with progress indicators."""
|
|
49
|
+
yield Header()
|
|
50
|
+
|
|
51
|
+
with Container(id="loading-container"):
|
|
52
|
+
# Main loading title
|
|
53
|
+
yield Label("GitFlow Analytics", classes="screen-title")
|
|
54
|
+
yield Label("Developer Productivity Analysis", classes="help-text center")
|
|
55
|
+
|
|
56
|
+
# Loading animation and message
|
|
57
|
+
with Center(), Vertical(id="loading-content"):
|
|
58
|
+
yield LoadingIndicator(id="main-spinner")
|
|
59
|
+
yield Label(self.loading_message, classes="center", id="loading-message")
|
|
60
|
+
|
|
61
|
+
# Overall progress bar
|
|
62
|
+
yield AnalysisProgressWidget("Startup Progress", total=100.0, id="startup-progress")
|
|
63
|
+
|
|
64
|
+
# Status messages
|
|
65
|
+
with Container(classes="status-panel"):
|
|
66
|
+
yield Label("Status", classes="panel-title")
|
|
67
|
+
yield Static("Starting up...", id="status-message")
|
|
68
|
+
|
|
69
|
+
# Loading steps indicators
|
|
70
|
+
with Container(classes="stats-panel"):
|
|
71
|
+
yield Label("Loading Steps", classes="panel-title")
|
|
72
|
+
with Vertical(id="loading-steps"):
|
|
73
|
+
yield Static("⏳ Initializing application...", id="step-init")
|
|
74
|
+
yield Static("⏳ Loading configuration...", id="step-config")
|
|
75
|
+
yield Static("⏳ Preparing analysis engine...", id="step-engine")
|
|
76
|
+
yield Static("⏳ Loading NLP models...", id="step-nlp")
|
|
77
|
+
yield Static("⏳ Finalizing setup...", id="step-finalize")
|
|
78
|
+
|
|
79
|
+
yield Footer()
|
|
80
|
+
|
|
81
|
+
def on_mount(self) -> None:
|
|
82
|
+
"""Start loading process when screen mounts."""
|
|
83
|
+
self.loading_task = asyncio.create_task(self._simulate_loading())
|
|
84
|
+
|
|
85
|
+
async def _simulate_loading(self) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Simulate the loading process with progress updates.
|
|
88
|
+
|
|
89
|
+
WHY: This provides visual feedback during startup initialization.
|
|
90
|
+
In practice, this would be replaced by actual initialization calls.
|
|
91
|
+
"""
|
|
92
|
+
progress_widget = self.query_one("#startup-progress", AnalysisProgressWidget)
|
|
93
|
+
status_message = self.query_one("#status-message", Static)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
# Step 1: Initialize application (20%)
|
|
97
|
+
await self._update_step("step-init", "✅ Application initialized", "success")
|
|
98
|
+
status_message.update("Initializing core components...")
|
|
99
|
+
progress_widget.update_progress(20, "Core components ready")
|
|
100
|
+
await asyncio.sleep(0.3)
|
|
101
|
+
|
|
102
|
+
if self.cancelled:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
# Step 2: Load configuration (40%)
|
|
106
|
+
await self._update_step("step-config", "✅ Configuration loaded", "success")
|
|
107
|
+
status_message.update("Loading configuration files...")
|
|
108
|
+
progress_widget.update_progress(40, "Configuration loaded")
|
|
109
|
+
await asyncio.sleep(0.5)
|
|
110
|
+
|
|
111
|
+
if self.cancelled:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# Step 3: Prepare analysis engine (60%)
|
|
115
|
+
await self._update_step("step-engine", "✅ Analysis engine ready", "success")
|
|
116
|
+
status_message.update("Preparing Git analysis engine...")
|
|
117
|
+
progress_widget.update_progress(60, "Analysis engine initialized")
|
|
118
|
+
await asyncio.sleep(0.4)
|
|
119
|
+
|
|
120
|
+
if self.cancelled:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
# Step 4: Load NLP models (85%) - This is the heavy operation
|
|
124
|
+
await self._update_step("step-nlp", "⏳ Loading spaCy models...", "warning")
|
|
125
|
+
status_message.update("Loading natural language processing models...")
|
|
126
|
+
progress_widget.update_progress(70, "Loading spaCy models...")
|
|
127
|
+
await asyncio.sleep(1.2) # Simulate spaCy model loading time
|
|
128
|
+
|
|
129
|
+
if self.cancelled:
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
await self._update_step("step-nlp", "✅ NLP models loaded", "success")
|
|
133
|
+
progress_widget.update_progress(85, "NLP models ready")
|
|
134
|
+
|
|
135
|
+
# Step 5: Finalize setup (100%)
|
|
136
|
+
await self._update_step("step-finalize", "✅ Setup complete", "success")
|
|
137
|
+
status_message.update("Finalizing application setup...")
|
|
138
|
+
progress_widget.update_progress(100, "GitFlow Analytics ready!")
|
|
139
|
+
await asyncio.sleep(0.3)
|
|
140
|
+
|
|
141
|
+
# Show completion message briefly
|
|
142
|
+
loading_message = self.query_one("#loading-message", Label)
|
|
143
|
+
loading_message.update("Loading complete! Starting application...")
|
|
144
|
+
|
|
145
|
+
elapsed_time = time.time() - self.start_time
|
|
146
|
+
status_message.update(f"Ready! Loaded in {elapsed_time:.1f} seconds")
|
|
147
|
+
|
|
148
|
+
# Wait a moment to show completion
|
|
149
|
+
await asyncio.sleep(0.8)
|
|
150
|
+
|
|
151
|
+
# Signal that loading is complete
|
|
152
|
+
self.app.post_message(self.LoadingComplete())
|
|
153
|
+
|
|
154
|
+
except asyncio.CancelledError:
|
|
155
|
+
progress_widget.update_progress(0, "Loading cancelled")
|
|
156
|
+
status_message.update("Loading cancelled by user")
|
|
157
|
+
except Exception as e:
|
|
158
|
+
progress_widget.update_progress(0, f"Error: {str(e)[:50]}...")
|
|
159
|
+
status_message.update(f"Loading failed: {e}")
|
|
160
|
+
self.app.notify(f"Loading failed: {e}", severity="error")
|
|
161
|
+
|
|
162
|
+
async def _update_step(self, step_id: str, message: str, status: str) -> None:
|
|
163
|
+
"""
|
|
164
|
+
Update a loading step with status.
|
|
165
|
+
|
|
166
|
+
@param step_id: ID of the step element to update
|
|
167
|
+
@param message: Status message to display
|
|
168
|
+
@param status: Status type (success, warning, error)
|
|
169
|
+
"""
|
|
170
|
+
step_element = self.query_one(f"#{step_id}", Static)
|
|
171
|
+
step_element.update(message)
|
|
172
|
+
|
|
173
|
+
# Apply appropriate styling based on status
|
|
174
|
+
step_element.remove_class("success", "warning", "error")
|
|
175
|
+
step_element.add_class(status)
|
|
176
|
+
|
|
177
|
+
def update_loading_message(self, message: str) -> None:
|
|
178
|
+
"""
|
|
179
|
+
Update the main loading message.
|
|
180
|
+
|
|
181
|
+
@param message: New loading message to display
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
loading_message = self.query_one("#loading-message", Label)
|
|
185
|
+
loading_message.update(message)
|
|
186
|
+
except Exception:
|
|
187
|
+
pass # Ignore if element not found
|
|
188
|
+
|
|
189
|
+
def update_progress(self, percentage: float, status: str) -> None:
|
|
190
|
+
"""
|
|
191
|
+
Update the overall progress bar.
|
|
192
|
+
|
|
193
|
+
@param percentage: Progress percentage (0-100)
|
|
194
|
+
@param status: Status message to display
|
|
195
|
+
"""
|
|
196
|
+
try:
|
|
197
|
+
progress_widget = self.query_one("#startup-progress", AnalysisProgressWidget)
|
|
198
|
+
progress_widget.update_progress(percentage, status)
|
|
199
|
+
except Exception:
|
|
200
|
+
pass # Ignore if element not found
|
|
201
|
+
|
|
202
|
+
def update_status(self, status: str) -> None:
|
|
203
|
+
"""
|
|
204
|
+
Update the status message.
|
|
205
|
+
|
|
206
|
+
@param status: Status message to display
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
status_message = self.query_one("#status-message", Static)
|
|
210
|
+
status_message.update(status)
|
|
211
|
+
except Exception:
|
|
212
|
+
pass # Ignore if element not found
|
|
213
|
+
|
|
214
|
+
def action_cancel(self) -> None:
|
|
215
|
+
"""Cancel the loading process."""
|
|
216
|
+
self.cancelled = True
|
|
217
|
+
if self.loading_task and not self.loading_task.done():
|
|
218
|
+
self.loading_task.cancel()
|
|
219
|
+
self.app.post_message(self.LoadingCancelled())
|
|
220
|
+
|
|
221
|
+
class LoadingComplete(Message):
|
|
222
|
+
"""Message sent when loading is complete."""
|
|
223
|
+
|
|
224
|
+
def __init__(self) -> None:
|
|
225
|
+
super().__init__()
|
|
226
|
+
|
|
227
|
+
class LoadingCancelled(Message):
|
|
228
|
+
"""Message sent when loading is cancelled."""
|
|
229
|
+
|
|
230
|
+
def __init__(self) -> None:
|
|
231
|
+
super().__init__()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class InitializationLoadingScreen(LoadingScreen):
|
|
235
|
+
"""
|
|
236
|
+
Specialized loading screen for real application initialization.
|
|
237
|
+
|
|
238
|
+
WHY: This version of the loading screen performs actual initialization
|
|
239
|
+
tasks instead of just simulating them, providing real progress feedback
|
|
240
|
+
during startup.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
def __init__(self, config_loader_func=None, nlp_init_func=None, *args, **kwargs) -> None:
|
|
244
|
+
super().__init__(*args, **kwargs)
|
|
245
|
+
self.config_loader_func = config_loader_func
|
|
246
|
+
self.nlp_init_func = nlp_init_func
|
|
247
|
+
self.initialization_data = {}
|
|
248
|
+
|
|
249
|
+
async def _simulate_loading(self) -> None:
|
|
250
|
+
"""
|
|
251
|
+
Perform actual initialization tasks with progress updates.
|
|
252
|
+
|
|
253
|
+
WHY: This replaces the simulation with real initialization work,
|
|
254
|
+
allowing the loading screen to show progress of actual operations.
|
|
255
|
+
"""
|
|
256
|
+
progress_widget = self.query_one("#startup-progress", AnalysisProgressWidget)
|
|
257
|
+
status_message = self.query_one("#status-message", Static)
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
# Step 1: Initialize application (20%)
|
|
261
|
+
await self._update_step("step-init", "✅ Application initialized", "success")
|
|
262
|
+
status_message.update("Initializing core components...")
|
|
263
|
+
progress_widget.update_progress(20, "Core components ready")
|
|
264
|
+
await asyncio.sleep(0.1)
|
|
265
|
+
|
|
266
|
+
if self.cancelled:
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
# Step 2: Load configuration (40%)
|
|
270
|
+
status_message.update("Discovering configuration files...")
|
|
271
|
+
await self._update_step("step-config", "⏳ Loading configuration...", "warning")
|
|
272
|
+
|
|
273
|
+
if self.config_loader_func:
|
|
274
|
+
config_result = await asyncio.get_event_loop().run_in_executor(
|
|
275
|
+
None, self.config_loader_func
|
|
276
|
+
)
|
|
277
|
+
self.initialization_data["config"] = config_result
|
|
278
|
+
|
|
279
|
+
await self._update_step("step-config", "✅ Configuration loaded", "success")
|
|
280
|
+
progress_widget.update_progress(40, "Configuration ready")
|
|
281
|
+
await asyncio.sleep(0.1)
|
|
282
|
+
|
|
283
|
+
if self.cancelled:
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
# Step 3: Prepare analysis engine (60%)
|
|
287
|
+
await self._update_step("step-engine", "✅ Analysis engine ready", "success")
|
|
288
|
+
status_message.update("Preparing Git analysis components...")
|
|
289
|
+
progress_widget.update_progress(60, "Analysis engine initialized")
|
|
290
|
+
await asyncio.sleep(0.2)
|
|
291
|
+
|
|
292
|
+
if self.cancelled:
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
# Step 4: Load NLP models (85%) - Heavy operation
|
|
296
|
+
config = self.initialization_data.get("config")
|
|
297
|
+
if config and getattr(config, "qualitative", None) and config.qualitative.enabled:
|
|
298
|
+
await self._update_step("step-nlp", "⏳ Loading spaCy models...", "warning")
|
|
299
|
+
status_message.update("Loading natural language processing models...")
|
|
300
|
+
progress_widget.update_progress(70, "Loading spaCy models...")
|
|
301
|
+
|
|
302
|
+
if self.nlp_init_func:
|
|
303
|
+
nlp_result = await asyncio.get_event_loop().run_in_executor(
|
|
304
|
+
None, self.nlp_init_func, config
|
|
305
|
+
)
|
|
306
|
+
self.initialization_data["nlp"] = nlp_result
|
|
307
|
+
|
|
308
|
+
await self._update_step("step-nlp", "✅ NLP models loaded", "success")
|
|
309
|
+
progress_widget.update_progress(85, "NLP models ready")
|
|
310
|
+
else:
|
|
311
|
+
await self._update_step("step-nlp", "⏸️ NLP models skipped", "warning")
|
|
312
|
+
progress_widget.update_progress(85, "NLP models skipped (qualitative disabled)")
|
|
313
|
+
|
|
314
|
+
if self.cancelled:
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
# Step 5: Finalize setup (100%)
|
|
318
|
+
await self._update_step("step-finalize", "✅ Setup complete", "success")
|
|
319
|
+
status_message.update("Finalizing application setup...")
|
|
320
|
+
progress_widget.update_progress(100, "GitFlow Analytics ready!")
|
|
321
|
+
|
|
322
|
+
# Show completion message briefly
|
|
323
|
+
loading_message = self.query_one("#loading-message", Label)
|
|
324
|
+
loading_message.update("Initialization complete! Starting application...")
|
|
325
|
+
|
|
326
|
+
elapsed_time = time.time() - self.start_time
|
|
327
|
+
status_message.update(f"Ready! Initialized in {elapsed_time:.1f} seconds")
|
|
328
|
+
|
|
329
|
+
# Wait a moment to show completion
|
|
330
|
+
await asyncio.sleep(0.5)
|
|
331
|
+
|
|
332
|
+
# Signal that loading is complete with initialization data
|
|
333
|
+
self.app.post_message(self.InitializationComplete(self.initialization_data))
|
|
334
|
+
|
|
335
|
+
except asyncio.CancelledError:
|
|
336
|
+
progress_widget.update_progress(0, "Initialization cancelled")
|
|
337
|
+
status_message.update("Initialization cancelled by user")
|
|
338
|
+
except Exception as e:
|
|
339
|
+
progress_widget.update_progress(0, f"Error: {str(e)[:50]}...")
|
|
340
|
+
status_message.update(f"Initialization failed: {e}")
|
|
341
|
+
self.app.notify(f"Initialization failed: {e}", severity="error")
|
|
342
|
+
|
|
343
|
+
class InitializationComplete(Message):
|
|
344
|
+
"""Message sent when initialization is complete with data."""
|
|
345
|
+
|
|
346
|
+
def __init__(self, data: dict[str, Any]) -> None:
|
|
347
|
+
super().__init__()
|
|
348
|
+
self.data = data
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Main screen for GitFlow Analytics TUI."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from textual.binding import Binding
|
|
7
|
+
from textual.containers import Container, Vertical
|
|
8
|
+
from textual.message import Message
|
|
9
|
+
from textual.screen import Screen
|
|
10
|
+
from textual.widgets import Button, Footer, Header, Label, Rule, Static
|
|
11
|
+
|
|
12
|
+
from gitflow_analytics.config import Config
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MainScreen(Screen):
|
|
16
|
+
"""
|
|
17
|
+
Main dashboard screen showing project information and navigation options.
|
|
18
|
+
|
|
19
|
+
WHY: Serves as the primary entry point for the TUI, providing users with
|
|
20
|
+
an overview of their configuration and clear navigation to all major features.
|
|
21
|
+
|
|
22
|
+
DESIGN DECISION: Uses a dashboard layout rather than a menu-driven approach
|
|
23
|
+
to provide immediate visibility into the current configuration status and
|
|
24
|
+
quick access to common operations.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
BINDINGS = [
|
|
28
|
+
Binding("ctrl+q", "quit", "Quit"),
|
|
29
|
+
Binding("ctrl+n", "new_analysis", "New Analysis"),
|
|
30
|
+
Binding("ctrl+o", "open_config", "Open Config"),
|
|
31
|
+
Binding("f1", "help", "Help"),
|
|
32
|
+
Binding("c", "cache_status", "Cache Status"),
|
|
33
|
+
Binding("i", "manage_identities", "Identities"),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
class NewAnalysisRequested(Message):
|
|
37
|
+
"""Message sent when new analysis is requested."""
|
|
38
|
+
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
super().__init__()
|
|
41
|
+
|
|
42
|
+
class ConfigurationRequested(Message):
|
|
43
|
+
"""Message sent when configuration is requested."""
|
|
44
|
+
|
|
45
|
+
def __init__(self) -> None:
|
|
46
|
+
super().__init__()
|
|
47
|
+
|
|
48
|
+
class CacheStatusRequested(Message):
|
|
49
|
+
"""Message sent when cache status is requested."""
|
|
50
|
+
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
super().__init__()
|
|
53
|
+
|
|
54
|
+
class IdentityManagementRequested(Message):
|
|
55
|
+
"""Message sent when identity management is requested."""
|
|
56
|
+
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
super().__init__()
|
|
59
|
+
|
|
60
|
+
class HelpRequested(Message):
|
|
61
|
+
"""Message sent when help is requested."""
|
|
62
|
+
|
|
63
|
+
def __init__(self) -> None:
|
|
64
|
+
super().__init__()
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
config: Optional[Config] = None,
|
|
69
|
+
config_path: Optional[Path] = None,
|
|
70
|
+
*,
|
|
71
|
+
name: Optional[str] = None,
|
|
72
|
+
id: Optional[str] = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
super().__init__(name=name, id=id)
|
|
75
|
+
self.config = config
|
|
76
|
+
self.config_path = config_path
|
|
77
|
+
|
|
78
|
+
def compose(self):
|
|
79
|
+
"""Compose the main screen."""
|
|
80
|
+
yield Header()
|
|
81
|
+
|
|
82
|
+
with Container(id="main-container"):
|
|
83
|
+
yield Label("GitFlow Analytics", classes="screen-title")
|
|
84
|
+
yield Static("Developer Productivity Analysis Tool", id="subtitle")
|
|
85
|
+
|
|
86
|
+
# Configuration status section
|
|
87
|
+
with Container(classes="status-panel"):
|
|
88
|
+
yield Label("Configuration Status", classes="section-title")
|
|
89
|
+
|
|
90
|
+
if self.config:
|
|
91
|
+
config_status = (
|
|
92
|
+
f"✅ Loaded: {self.config_path}"
|
|
93
|
+
if self.config_path
|
|
94
|
+
else "✅ Configuration loaded"
|
|
95
|
+
)
|
|
96
|
+
yield Static(config_status, id="config-status")
|
|
97
|
+
|
|
98
|
+
# Show key configuration details
|
|
99
|
+
details = self._get_config_summary()
|
|
100
|
+
yield Static(details, id="config-details")
|
|
101
|
+
else:
|
|
102
|
+
yield Static("❌ No configuration loaded", id="config-status")
|
|
103
|
+
yield Static(
|
|
104
|
+
"Load or create a configuration to begin analysis", id="config-help"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
yield Rule()
|
|
108
|
+
|
|
109
|
+
# Main actions section
|
|
110
|
+
with Container(classes="actions-panel"):
|
|
111
|
+
yield Label("Available Actions", classes="section-title")
|
|
112
|
+
|
|
113
|
+
with Vertical(id="action-buttons"):
|
|
114
|
+
if self.config:
|
|
115
|
+
yield Button("🚀 Run Analysis", variant="primary", id="run-analysis")
|
|
116
|
+
yield Button("📊 View Cache Status", id="cache-status")
|
|
117
|
+
yield Button("👥 Manage Developer Identities", id="manage-identities")
|
|
118
|
+
yield Button("⚙️ Edit Configuration", id="edit-config")
|
|
119
|
+
else:
|
|
120
|
+
yield Button("📁 Load Configuration", variant="primary", id="load-config")
|
|
121
|
+
yield Button("➕ Create New Configuration", id="new-config")
|
|
122
|
+
|
|
123
|
+
yield Rule()
|
|
124
|
+
yield Button("❓ Help & Documentation", id="help")
|
|
125
|
+
|
|
126
|
+
# Quick stats section (if config loaded)
|
|
127
|
+
if self.config:
|
|
128
|
+
with Container(classes="stats-panel"):
|
|
129
|
+
yield Label("Quick Information", classes="section-title")
|
|
130
|
+
stats = self._get_quick_stats()
|
|
131
|
+
yield Static(stats, id="quick-stats")
|
|
132
|
+
|
|
133
|
+
yield Footer()
|
|
134
|
+
|
|
135
|
+
def _get_config_summary(self) -> str:
|
|
136
|
+
"""
|
|
137
|
+
Generate configuration summary for display.
|
|
138
|
+
|
|
139
|
+
WHY: Provides users with immediate visibility into their current
|
|
140
|
+
configuration without needing to navigate to a separate screen.
|
|
141
|
+
"""
|
|
142
|
+
if not self.config:
|
|
143
|
+
return ""
|
|
144
|
+
|
|
145
|
+
lines = []
|
|
146
|
+
|
|
147
|
+
# Repository count
|
|
148
|
+
repo_count = len(self.config.repositories) if self.config.repositories else 0
|
|
149
|
+
if self.config.github.organization and not self.config.repositories:
|
|
150
|
+
lines.append(f"• Organization: {self.config.github.organization} (auto-discovery)")
|
|
151
|
+
else:
|
|
152
|
+
lines.append(f"• Repositories: {repo_count} configured")
|
|
153
|
+
|
|
154
|
+
# GitHub integration
|
|
155
|
+
if self.config.github.token:
|
|
156
|
+
lines.append("✅ GitHub API configured")
|
|
157
|
+
else:
|
|
158
|
+
lines.append("⚠️ GitHub API not configured")
|
|
159
|
+
|
|
160
|
+
# Qualitative analysis
|
|
161
|
+
if (
|
|
162
|
+
hasattr(self.config, "qualitative")
|
|
163
|
+
and self.config.qualitative
|
|
164
|
+
and self.config.qualitative.enabled
|
|
165
|
+
):
|
|
166
|
+
lines.append("✅ Qualitative analysis enabled")
|
|
167
|
+
else:
|
|
168
|
+
lines.append("⚠️ Qualitative analysis disabled")
|
|
169
|
+
|
|
170
|
+
# JIRA integration
|
|
171
|
+
if hasattr(self.config, "jira") and self.config.jira and self.config.jira.base_url:
|
|
172
|
+
lines.append("✅ JIRA integration configured")
|
|
173
|
+
else:
|
|
174
|
+
lines.append("⚠️ JIRA integration not configured")
|
|
175
|
+
|
|
176
|
+
# Cache configuration
|
|
177
|
+
lines.append(f"• Cache TTL: {self.config.cache.ttl_hours}h")
|
|
178
|
+
|
|
179
|
+
return "\n".join(lines)
|
|
180
|
+
|
|
181
|
+
def _get_quick_stats(self) -> str:
|
|
182
|
+
"""
|
|
183
|
+
Generate quick statistics if cache data is available.
|
|
184
|
+
|
|
185
|
+
WHY: Shows users what data is already available from previous runs,
|
|
186
|
+
helping them understand if they need to run a new analysis.
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
from ....core.cache import GitAnalysisCache
|
|
190
|
+
|
|
191
|
+
cache = GitAnalysisCache(self.config.cache.directory)
|
|
192
|
+
stats = cache.get_cache_stats()
|
|
193
|
+
|
|
194
|
+
lines = []
|
|
195
|
+
lines.append(f"• Cached commits: {stats['cached_commits']:,}")
|
|
196
|
+
lines.append(f"• Cached PRs: {stats['cached_prs']:,}")
|
|
197
|
+
lines.append(f"• Cached issues: {stats['cached_issues']:,}")
|
|
198
|
+
|
|
199
|
+
if stats["stale_commits"] > 0:
|
|
200
|
+
lines.append(f"• Stale entries: {stats['stale_commits']:,}")
|
|
201
|
+
|
|
202
|
+
# Cache size
|
|
203
|
+
import os
|
|
204
|
+
|
|
205
|
+
cache_size = 0
|
|
206
|
+
try:
|
|
207
|
+
for root, _dirs, files in os.walk(self.config.cache.directory):
|
|
208
|
+
for f in files:
|
|
209
|
+
cache_size += os.path.getsize(os.path.join(root, f))
|
|
210
|
+
cache_size_mb = cache_size / 1024 / 1024
|
|
211
|
+
lines.append(f"• Cache size: {cache_size_mb:.1f} MB")
|
|
212
|
+
except Exception:
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
return "\n".join(lines)
|
|
216
|
+
|
|
217
|
+
except Exception:
|
|
218
|
+
return "Cache statistics unavailable"
|
|
219
|
+
|
|
220
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
221
|
+
"""Handle button press events."""
|
|
222
|
+
button_actions = {
|
|
223
|
+
"run-analysis": self._run_analysis,
|
|
224
|
+
"load-config": self._load_config,
|
|
225
|
+
"new-config": self._new_config,
|
|
226
|
+
"edit-config": self._edit_config,
|
|
227
|
+
"cache-status": self._cache_status,
|
|
228
|
+
"manage-identities": self._manage_identities,
|
|
229
|
+
"help": self._show_help,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
action = button_actions.get(event.button.id)
|
|
233
|
+
if action:
|
|
234
|
+
action()
|
|
235
|
+
|
|
236
|
+
def _run_analysis(self) -> None:
|
|
237
|
+
"""Request new analysis."""
|
|
238
|
+
if not self.config:
|
|
239
|
+
self.notify("Please load or create a configuration first", severity="error")
|
|
240
|
+
return
|
|
241
|
+
self.post_message(self.NewAnalysisRequested())
|
|
242
|
+
|
|
243
|
+
def _load_config(self) -> None:
|
|
244
|
+
"""Request configuration loading."""
|
|
245
|
+
self.post_message(self.ConfigurationRequested())
|
|
246
|
+
|
|
247
|
+
def _new_config(self) -> None:
|
|
248
|
+
"""Request new configuration creation."""
|
|
249
|
+
self.post_message(self.ConfigurationRequested())
|
|
250
|
+
|
|
251
|
+
def _edit_config(self) -> None:
|
|
252
|
+
"""Request configuration editing."""
|
|
253
|
+
self.post_message(self.ConfigurationRequested())
|
|
254
|
+
|
|
255
|
+
def _cache_status(self) -> None:
|
|
256
|
+
"""Request cache status display."""
|
|
257
|
+
self.post_message(self.CacheStatusRequested())
|
|
258
|
+
|
|
259
|
+
def _manage_identities(self) -> None:
|
|
260
|
+
"""Request identity management."""
|
|
261
|
+
self.post_message(self.IdentityManagementRequested())
|
|
262
|
+
|
|
263
|
+
def _show_help(self) -> None:
|
|
264
|
+
"""Request help display."""
|
|
265
|
+
self.post_message(self.HelpRequested())
|
|
266
|
+
|
|
267
|
+
# Action handlers for key bindings
|
|
268
|
+
def action_quit(self) -> None:
|
|
269
|
+
"""Quit the application."""
|
|
270
|
+
self.app.exit()
|
|
271
|
+
|
|
272
|
+
def action_new_analysis(self) -> None:
|
|
273
|
+
"""Start new analysis via keyboard shortcut."""
|
|
274
|
+
self._run_analysis()
|
|
275
|
+
|
|
276
|
+
def action_open_config(self) -> None:
|
|
277
|
+
"""Open configuration via keyboard shortcut."""
|
|
278
|
+
self._load_config()
|
|
279
|
+
|
|
280
|
+
def action_help(self) -> None:
|
|
281
|
+
"""Show help via keyboard shortcut."""
|
|
282
|
+
self._show_help()
|
|
283
|
+
|
|
284
|
+
def action_cache_status(self) -> None:
|
|
285
|
+
"""Show cache status via keyboard shortcut."""
|
|
286
|
+
self._cache_status()
|
|
287
|
+
|
|
288
|
+
def action_manage_identities(self) -> None:
|
|
289
|
+
"""Manage identities via keyboard shortcut."""
|
|
290
|
+
self._manage_identities()
|
|
291
|
+
|
|
292
|
+
def update_config(self, config: Config, config_path: Optional[Path] = None) -> None:
|
|
293
|
+
"""
|
|
294
|
+
Update the configuration and refresh the display.
|
|
295
|
+
|
|
296
|
+
WHY: Allows the main screen to be updated when configuration changes
|
|
297
|
+
without requiring a full screen rebuild, maintaining user context.
|
|
298
|
+
"""
|
|
299
|
+
self.config = config
|
|
300
|
+
self.config_path = config_path
|
|
301
|
+
|
|
302
|
+
# Update configuration status
|
|
303
|
+
if self.config:
|
|
304
|
+
config_status = (
|
|
305
|
+
f"✅ Loaded: {self.config_path}" if self.config_path else "✅ Configuration loaded"
|
|
306
|
+
)
|
|
307
|
+
self.query_one("#config-status", Static).update(config_status)
|
|
308
|
+
|
|
309
|
+
# Update configuration details
|
|
310
|
+
details = self._get_config_summary()
|
|
311
|
+
self.query_one("#config-details", Static).update(details)
|
|
312
|
+
|
|
313
|
+
# Update quick stats if available
|
|
314
|
+
try:
|
|
315
|
+
stats = self._get_quick_stats()
|
|
316
|
+
self.query_one("#quick-stats", Static).update(stats)
|
|
317
|
+
except Exception:
|
|
318
|
+
pass
|
|
319
|
+
|
|
320
|
+
# Refresh to rebuild buttons with new state
|
|
321
|
+
self.refresh(recompose=True)
|