gitflow-analytics 1.0.0__py3-none-any.whl → 1.0.3__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 -9
- gitflow_analytics/_version.py +2 -2
- gitflow_analytics/cli.py +691 -243
- gitflow_analytics/cli_rich.py +353 -0
- gitflow_analytics/config.py +389 -96
- gitflow_analytics/core/analyzer.py +175 -78
- gitflow_analytics/core/branch_mapper.py +132 -132
- gitflow_analytics/core/cache.py +242 -173
- gitflow_analytics/core/identity.py +214 -178
- gitflow_analytics/extractors/base.py +13 -11
- gitflow_analytics/extractors/story_points.py +70 -59
- gitflow_analytics/extractors/tickets.py +111 -88
- gitflow_analytics/integrations/github_integration.py +91 -77
- gitflow_analytics/integrations/jira_integration.py +284 -0
- gitflow_analytics/integrations/orchestrator.py +99 -72
- gitflow_analytics/metrics/dora.py +183 -179
- gitflow_analytics/models/database.py +191 -54
- gitflow_analytics/qualitative/__init__.py +30 -0
- gitflow_analytics/qualitative/classifiers/__init__.py +13 -0
- gitflow_analytics/qualitative/classifiers/change_type.py +468 -0
- gitflow_analytics/qualitative/classifiers/domain_classifier.py +399 -0
- gitflow_analytics/qualitative/classifiers/intent_analyzer.py +436 -0
- gitflow_analytics/qualitative/classifiers/risk_analyzer.py +412 -0
- gitflow_analytics/qualitative/core/__init__.py +13 -0
- gitflow_analytics/qualitative/core/llm_fallback.py +653 -0
- gitflow_analytics/qualitative/core/nlp_engine.py +373 -0
- gitflow_analytics/qualitative/core/pattern_cache.py +457 -0
- gitflow_analytics/qualitative/core/processor.py +540 -0
- gitflow_analytics/qualitative/models/__init__.py +25 -0
- gitflow_analytics/qualitative/models/schemas.py +272 -0
- gitflow_analytics/qualitative/utils/__init__.py +13 -0
- gitflow_analytics/qualitative/utils/batch_processor.py +326 -0
- gitflow_analytics/qualitative/utils/cost_tracker.py +343 -0
- gitflow_analytics/qualitative/utils/metrics.py +347 -0
- gitflow_analytics/qualitative/utils/text_processing.py +243 -0
- gitflow_analytics/reports/analytics_writer.py +25 -8
- gitflow_analytics/reports/csv_writer.py +60 -32
- gitflow_analytics/reports/narrative_writer.py +21 -15
- gitflow_analytics/tui/__init__.py +5 -0
- gitflow_analytics/tui/app.py +721 -0
- gitflow_analytics/tui/screens/__init__.py +8 -0
- gitflow_analytics/tui/screens/analysis_progress_screen.py +487 -0
- gitflow_analytics/tui/screens/configuration_screen.py +547 -0
- gitflow_analytics/tui/screens/loading_screen.py +358 -0
- gitflow_analytics/tui/screens/main_screen.py +304 -0
- gitflow_analytics/tui/screens/results_screen.py +698 -0
- gitflow_analytics/tui/widgets/__init__.py +7 -0
- gitflow_analytics/tui/widgets/data_table.py +257 -0
- gitflow_analytics/tui/widgets/export_modal.py +301 -0
- gitflow_analytics/tui/widgets/progress_widget.py +192 -0
- gitflow_analytics-1.0.3.dist-info/METADATA +490 -0
- gitflow_analytics-1.0.3.dist-info/RECORD +62 -0
- gitflow_analytics-1.0.0.dist-info/METADATA +0 -201
- gitflow_analytics-1.0.0.dist-info/RECORD +0 -30
- {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/WHEEL +0 -0
- {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
"""Configuration screen for GitFlow Analytics TUI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Dict, Any
|
|
6
|
+
|
|
7
|
+
from textual.widgets import (
|
|
8
|
+
Header, Footer, Button, Input, Label, Switch,
|
|
9
|
+
TabbedContent, TabPane, Rule, Static
|
|
10
|
+
)
|
|
11
|
+
from textual.containers import Container, Horizontal, Vertical
|
|
12
|
+
from textual.screen import ModalScreen
|
|
13
|
+
from textual.binding import Binding
|
|
14
|
+
from textual.validation import Function, ValidationResult
|
|
15
|
+
|
|
16
|
+
from gitflow_analytics.config import ConfigLoader, Config
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConfigurationScreen(ModalScreen[Optional[Config]]):
|
|
20
|
+
"""
|
|
21
|
+
Modal screen for comprehensive configuration management.
|
|
22
|
+
|
|
23
|
+
WHY: Configuration is complex with multiple categories (API keys, analysis
|
|
24
|
+
settings, cache options) that benefit from a tabbed interface. Modal design
|
|
25
|
+
ensures focused interaction without losing main screen context.
|
|
26
|
+
|
|
27
|
+
DESIGN DECISION: Uses a tabbed interface to organize related settings and
|
|
28
|
+
provides real-time validation to prevent configuration errors. Supports both
|
|
29
|
+
loading existing configurations and creating new ones from scratch.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
DEFAULT_CSS = """
|
|
33
|
+
ConfigurationScreen {
|
|
34
|
+
align: center middle;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#config-modal {
|
|
38
|
+
background: $surface;
|
|
39
|
+
border: thick $primary;
|
|
40
|
+
width: 90%;
|
|
41
|
+
height: 85%;
|
|
42
|
+
padding: 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.modal-title {
|
|
46
|
+
text-align: center;
|
|
47
|
+
text-style: bold;
|
|
48
|
+
color: $primary;
|
|
49
|
+
margin-bottom: 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.section-title {
|
|
53
|
+
text-style: bold;
|
|
54
|
+
color: $secondary;
|
|
55
|
+
margin: 1 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.form-row {
|
|
59
|
+
height: 3;
|
|
60
|
+
margin: 1 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.form-label {
|
|
64
|
+
width: 25;
|
|
65
|
+
padding: 1 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.form-input {
|
|
69
|
+
width: 1fr;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.button-bar {
|
|
73
|
+
dock: bottom;
|
|
74
|
+
height: 3;
|
|
75
|
+
align: center middle;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.help-text {
|
|
79
|
+
color: $text-muted;
|
|
80
|
+
text-style: italic;
|
|
81
|
+
margin: 0 0 1 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.validation-error {
|
|
85
|
+
color: $error;
|
|
86
|
+
text-style: bold;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.validation-success {
|
|
90
|
+
color: $success;
|
|
91
|
+
text-style: bold;
|
|
92
|
+
}
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
BINDINGS = [
|
|
96
|
+
Binding("escape", "cancel", "Cancel"),
|
|
97
|
+
Binding("ctrl+s", "save", "Save Config"),
|
|
98
|
+
Binding("ctrl+t", "test_connection", "Test Connections"),
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
config_path: Optional[Path] = None,
|
|
104
|
+
config: Optional[Config] = None
|
|
105
|
+
) -> None:
|
|
106
|
+
super().__init__()
|
|
107
|
+
self.config_path = config_path
|
|
108
|
+
self.config = config
|
|
109
|
+
self.form_data = {}
|
|
110
|
+
self.validation_errors = {}
|
|
111
|
+
|
|
112
|
+
def compose(self):
|
|
113
|
+
"""Compose the configuration modal."""
|
|
114
|
+
with Container(id="config-modal"):
|
|
115
|
+
yield Label("GitFlow Analytics Configuration", classes="modal-title")
|
|
116
|
+
|
|
117
|
+
with TabbedContent():
|
|
118
|
+
# Basic Settings Tab
|
|
119
|
+
with TabPane("Basic Settings", id="basic"):
|
|
120
|
+
yield from self._compose_basic_settings()
|
|
121
|
+
|
|
122
|
+
# API Keys Tab
|
|
123
|
+
with TabPane("API Keys", id="api-keys"):
|
|
124
|
+
yield from self._compose_api_keys()
|
|
125
|
+
|
|
126
|
+
# Analysis Settings Tab
|
|
127
|
+
with TabPane("Analysis", id="analysis"):
|
|
128
|
+
yield from self._compose_analysis_settings()
|
|
129
|
+
|
|
130
|
+
# Cache Settings Tab
|
|
131
|
+
with TabPane("Cache", id="cache"):
|
|
132
|
+
yield from self._compose_cache_settings()
|
|
133
|
+
|
|
134
|
+
# Repository Settings Tab
|
|
135
|
+
with TabPane("Repositories", id="repositories"):
|
|
136
|
+
yield from self._compose_repository_settings()
|
|
137
|
+
|
|
138
|
+
# Validation status
|
|
139
|
+
with Container(id="validation-status"):
|
|
140
|
+
yield Static("", id="validation-message")
|
|
141
|
+
|
|
142
|
+
# Button bar
|
|
143
|
+
with Horizontal(classes="button-bar"):
|
|
144
|
+
yield Button("Test Connections", id="test-btn")
|
|
145
|
+
yield Button("Cancel", variant="default", id="cancel-btn")
|
|
146
|
+
yield Button("Save Configuration", variant="primary", id="save-btn")
|
|
147
|
+
|
|
148
|
+
def _compose_basic_settings(self):
|
|
149
|
+
"""Compose basic settings tab."""
|
|
150
|
+
# Configuration file path
|
|
151
|
+
yield Label("Configuration File:", classes="section-title")
|
|
152
|
+
yield Static(
|
|
153
|
+
"Path where the configuration will be saved",
|
|
154
|
+
classes="help-text"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
with Horizontal(classes="form-row"):
|
|
158
|
+
yield Label("Config Path:", classes="form-label")
|
|
159
|
+
yield Input(
|
|
160
|
+
value=str(self.config_path) if self.config_path else "config.yaml",
|
|
161
|
+
placeholder="Path to config.yaml file",
|
|
162
|
+
classes="form-input",
|
|
163
|
+
id="config-path"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Analysis period
|
|
167
|
+
yield Rule()
|
|
168
|
+
yield Label("Analysis Settings:", classes="section-title")
|
|
169
|
+
|
|
170
|
+
with Horizontal(classes="form-row"):
|
|
171
|
+
yield Label("Analysis Period (weeks):", classes="form-label")
|
|
172
|
+
yield Input(
|
|
173
|
+
value="12",
|
|
174
|
+
placeholder="12",
|
|
175
|
+
classes="form-input",
|
|
176
|
+
id="weeks",
|
|
177
|
+
validators=[Function(self._validate_positive_integer, "Must be a positive integer")]
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
with Horizontal(classes="form-row"):
|
|
181
|
+
yield Label("Enable Qualitative Analysis:", classes="form-label")
|
|
182
|
+
yield Switch(value=True, id="enable-qualitative")
|
|
183
|
+
|
|
184
|
+
yield Static(
|
|
185
|
+
"Qualitative analysis provides deeper insights into commit patterns and code changes",
|
|
186
|
+
classes="help-text"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def _compose_api_keys(self):
|
|
190
|
+
"""Compose API keys tab."""
|
|
191
|
+
# GitHub
|
|
192
|
+
yield Label("GitHub Configuration:", classes="section-title")
|
|
193
|
+
yield Static(
|
|
194
|
+
"Required for accessing GitHub repositories and pull request data",
|
|
195
|
+
classes="help-text"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
with Horizontal(classes="form-row"):
|
|
199
|
+
yield Label("Personal Access Token:", classes="form-label")
|
|
200
|
+
yield Input(
|
|
201
|
+
placeholder="ghp_xxxxxxxxxxxxxxxxxxxx",
|
|
202
|
+
password=True,
|
|
203
|
+
classes="form-input",
|
|
204
|
+
id="github-token"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
with Horizontal(classes="form-row"):
|
|
208
|
+
yield Label("Organization (optional):", classes="form-label")
|
|
209
|
+
yield Input(
|
|
210
|
+
placeholder="your-organization",
|
|
211
|
+
classes="form-input",
|
|
212
|
+
id="github-org"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# OpenRouter (for qualitative analysis)
|
|
216
|
+
yield Rule()
|
|
217
|
+
yield Label("OpenRouter Configuration:", classes="section-title")
|
|
218
|
+
yield Static(
|
|
219
|
+
"Required for AI-powered qualitative analysis of commit messages",
|
|
220
|
+
classes="help-text"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
with Horizontal(classes="form-row"):
|
|
224
|
+
yield Label("API Key:", classes="form-label")
|
|
225
|
+
yield Input(
|
|
226
|
+
placeholder="sk-or-xxxxxxxxxxxxxxxxxxxx",
|
|
227
|
+
password=True,
|
|
228
|
+
classes="form-input",
|
|
229
|
+
id="openrouter-key"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# JIRA
|
|
233
|
+
yield Rule()
|
|
234
|
+
yield Label("JIRA Configuration (Optional):", classes="section-title")
|
|
235
|
+
yield Static(
|
|
236
|
+
"Optional: Connect to JIRA for enhanced ticket tracking and story point analysis",
|
|
237
|
+
classes="help-text"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
with Horizontal(classes="form-row"):
|
|
241
|
+
yield Label("Base URL:", classes="form-label")
|
|
242
|
+
yield Input(
|
|
243
|
+
placeholder="https://company.atlassian.net",
|
|
244
|
+
classes="form-input",
|
|
245
|
+
id="jira-url"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
with Horizontal(classes="form-row"):
|
|
249
|
+
yield Label("Username/Email:", classes="form-label")
|
|
250
|
+
yield Input(
|
|
251
|
+
placeholder="your.email@company.com",
|
|
252
|
+
classes="form-input",
|
|
253
|
+
id="jira-user"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
with Horizontal(classes="form-row"):
|
|
257
|
+
yield Label("API Token:", classes="form-label")
|
|
258
|
+
yield Input(
|
|
259
|
+
placeholder="ATATT3xFfGF0...",
|
|
260
|
+
password=True,
|
|
261
|
+
classes="form-input",
|
|
262
|
+
id="jira-token"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
def _compose_analysis_settings(self):
|
|
266
|
+
"""Compose analysis settings tab."""
|
|
267
|
+
yield Label("Developer Identity Resolution:", classes="section-title")
|
|
268
|
+
|
|
269
|
+
with Horizontal(classes="form-row"):
|
|
270
|
+
yield Label("Similarity Threshold:", classes="form-label")
|
|
271
|
+
yield Input(
|
|
272
|
+
value="0.85",
|
|
273
|
+
placeholder="0.85",
|
|
274
|
+
classes="form-input",
|
|
275
|
+
id="similarity-threshold",
|
|
276
|
+
validators=[Function(self._validate_float_0_1, "Must be between 0.0 and 1.0")]
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
yield Static(
|
|
280
|
+
"Threshold for fuzzy matching of developer names (0.0 = loose, 1.0 = exact)",
|
|
281
|
+
classes="help-text"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
yield Rule()
|
|
285
|
+
yield Label("Ticket Platform Settings:", classes="section-title")
|
|
286
|
+
|
|
287
|
+
with Horizontal(classes="form-row"):
|
|
288
|
+
yield Label("Allowed Platforms:", classes="form-label")
|
|
289
|
+
yield Input(
|
|
290
|
+
placeholder="jira,github,linear (comma-separated)",
|
|
291
|
+
classes="form-input",
|
|
292
|
+
id="ticket-platforms"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
yield Rule()
|
|
296
|
+
yield Label("Path Exclusions:", classes="section-title")
|
|
297
|
+
|
|
298
|
+
with Horizontal(classes="form-row"):
|
|
299
|
+
yield Label("Exclude Paths:", classes="form-label")
|
|
300
|
+
yield Input(
|
|
301
|
+
placeholder="node_modules,dist,build (comma-separated)",
|
|
302
|
+
classes="form-input",
|
|
303
|
+
id="exclude-paths"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def _compose_cache_settings(self):
|
|
307
|
+
"""Compose cache settings tab."""
|
|
308
|
+
yield Label("Cache Configuration:", classes="section-title")
|
|
309
|
+
yield Static(
|
|
310
|
+
"Caching improves performance by storing analyzed data locally",
|
|
311
|
+
classes="help-text"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
with Horizontal(classes="form-row"):
|
|
315
|
+
yield Label("Cache Directory:", classes="form-label")
|
|
316
|
+
yield Input(
|
|
317
|
+
value=".gitflow-cache",
|
|
318
|
+
placeholder=".gitflow-cache",
|
|
319
|
+
classes="form-input",
|
|
320
|
+
id="cache-dir"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
with Horizontal(classes="form-row"):
|
|
324
|
+
yield Label("Cache TTL (hours):", classes="form-label")
|
|
325
|
+
yield Input(
|
|
326
|
+
value="168",
|
|
327
|
+
placeholder="168",
|
|
328
|
+
classes="form-input",
|
|
329
|
+
id="cache-ttl",
|
|
330
|
+
validators=[Function(self._validate_positive_integer, "Must be a positive integer")]
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
with Horizontal(classes="form-row"):
|
|
334
|
+
yield Label("Clear cache on startup:", classes="form-label")
|
|
335
|
+
yield Switch(value=False, id="clear-cache")
|
|
336
|
+
|
|
337
|
+
yield Static(
|
|
338
|
+
"168 hours = 1 week. Cached data older than this will be refreshed.",
|
|
339
|
+
classes="help-text"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def _compose_repository_settings(self):
|
|
343
|
+
"""Compose repository settings tab."""
|
|
344
|
+
yield Label("Repository Discovery:", classes="section-title")
|
|
345
|
+
yield Static(
|
|
346
|
+
"Configure how repositories are discovered and analyzed",
|
|
347
|
+
classes="help-text"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
with Horizontal(classes="form-row"):
|
|
351
|
+
yield Label("Auto-discover from org:", classes="form-label")
|
|
352
|
+
yield Switch(value=True, id="auto-discover")
|
|
353
|
+
|
|
354
|
+
yield Static(
|
|
355
|
+
"When enabled, automatically discovers all repositories from the GitHub organization",
|
|
356
|
+
classes="help-text"
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
yield Rule()
|
|
360
|
+
yield Label("Manual Repository Configuration:", classes="section-title")
|
|
361
|
+
yield Static(
|
|
362
|
+
"Add individual repositories manually (overrides organization discovery)",
|
|
363
|
+
classes="help-text"
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# TODO: Add dynamic repository list management
|
|
367
|
+
yield Static(
|
|
368
|
+
"Manual repository configuration will be available in a future version",
|
|
369
|
+
classes="help-text"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def _validate_positive_integer(self, value: str) -> ValidationResult:
|
|
373
|
+
"""Validate positive integer input."""
|
|
374
|
+
try:
|
|
375
|
+
int_val = int(value)
|
|
376
|
+
if int_val > 0:
|
|
377
|
+
return ValidationResult.success()
|
|
378
|
+
else:
|
|
379
|
+
return ValidationResult.error("Must be greater than 0")
|
|
380
|
+
except ValueError:
|
|
381
|
+
return ValidationResult.error("Must be a valid integer")
|
|
382
|
+
|
|
383
|
+
def _validate_float_0_1(self, value: str) -> ValidationResult:
|
|
384
|
+
"""Validate float value between 0.0 and 1.0."""
|
|
385
|
+
try:
|
|
386
|
+
float_val = float(value)
|
|
387
|
+
if 0.0 <= float_val <= 1.0:
|
|
388
|
+
return ValidationResult.success()
|
|
389
|
+
else:
|
|
390
|
+
return ValidationResult.error("Must be between 0.0 and 1.0")
|
|
391
|
+
except ValueError:
|
|
392
|
+
return ValidationResult.error("Must be a valid number")
|
|
393
|
+
|
|
394
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
395
|
+
"""Handle button press events."""
|
|
396
|
+
if event.button.id == "cancel-btn":
|
|
397
|
+
self.action_cancel()
|
|
398
|
+
elif event.button.id == "save-btn":
|
|
399
|
+
self.action_save()
|
|
400
|
+
elif event.button.id == "test-btn":
|
|
401
|
+
self.action_test_connection()
|
|
402
|
+
|
|
403
|
+
def action_cancel(self) -> None:
|
|
404
|
+
"""Cancel configuration changes."""
|
|
405
|
+
self.dismiss(None)
|
|
406
|
+
|
|
407
|
+
def action_save(self) -> None:
|
|
408
|
+
"""
|
|
409
|
+
Save configuration after validation.
|
|
410
|
+
|
|
411
|
+
WHY: Performs comprehensive validation before saving to prevent
|
|
412
|
+
runtime errors and provides clear feedback about any issues.
|
|
413
|
+
"""
|
|
414
|
+
try:
|
|
415
|
+
# Collect form data
|
|
416
|
+
form_data = self._collect_form_data()
|
|
417
|
+
|
|
418
|
+
# Validate form data
|
|
419
|
+
validation_result = self._validate_form_data(form_data)
|
|
420
|
+
if not validation_result['valid']:
|
|
421
|
+
self._show_validation_errors(validation_result['errors'])
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
# Update environment variables
|
|
425
|
+
self._update_env_vars(form_data)
|
|
426
|
+
|
|
427
|
+
# Create or load configuration
|
|
428
|
+
config = self._create_config_from_form(form_data)
|
|
429
|
+
|
|
430
|
+
# Save configuration file if path provided
|
|
431
|
+
config_path = form_data.get('config_path')
|
|
432
|
+
if config_path:
|
|
433
|
+
config_path = Path(config_path)
|
|
434
|
+
# TODO: Actually save config to file
|
|
435
|
+
# ConfigLoader.save(config, config_path)
|
|
436
|
+
|
|
437
|
+
self.dismiss(config)
|
|
438
|
+
|
|
439
|
+
except Exception as e:
|
|
440
|
+
self.notify(f"Configuration error: {e}", severity="error")
|
|
441
|
+
|
|
442
|
+
def action_test_connection(self) -> None:
|
|
443
|
+
"""Test API connections with current settings."""
|
|
444
|
+
self.notify("Connection testing not yet implemented", severity="info")
|
|
445
|
+
# TODO: Implement connection testing
|
|
446
|
+
|
|
447
|
+
def _collect_form_data(self) -> Dict[str, Any]:
|
|
448
|
+
"""Collect all form data from inputs."""
|
|
449
|
+
return {
|
|
450
|
+
'config_path': self.query_one("#config-path", Input).value,
|
|
451
|
+
'weeks': self.query_one("#weeks", Input).value,
|
|
452
|
+
'enable_qualitative': self.query_one("#enable-qualitative", Switch).value,
|
|
453
|
+
'github_token': self.query_one("#github-token", Input).value,
|
|
454
|
+
'github_org': self.query_one("#github-org", Input).value,
|
|
455
|
+
'openrouter_key': self.query_one("#openrouter-key", Input).value,
|
|
456
|
+
'jira_url': self.query_one("#jira-url", Input).value,
|
|
457
|
+
'jira_user': self.query_one("#jira-user", Input).value,
|
|
458
|
+
'jira_token': self.query_one("#jira-token", Input).value,
|
|
459
|
+
'similarity_threshold': self.query_one("#similarity-threshold", Input).value,
|
|
460
|
+
'ticket_platforms': self.query_one("#ticket-platforms", Input).value,
|
|
461
|
+
'exclude_paths': self.query_one("#exclude-paths", Input).value,
|
|
462
|
+
'cache_dir': self.query_one("#cache-dir", Input).value,
|
|
463
|
+
'cache_ttl': self.query_one("#cache-ttl", Input).value,
|
|
464
|
+
'clear_cache': self.query_one("#clear-cache", Switch).value,
|
|
465
|
+
'auto_discover': self.query_one("#auto-discover", Switch).value,
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
def _validate_form_data(self, form_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
469
|
+
"""Validate collected form data."""
|
|
470
|
+
errors = []
|
|
471
|
+
|
|
472
|
+
# Required fields validation
|
|
473
|
+
if not form_data['config_path']:
|
|
474
|
+
errors.append("Configuration path is required")
|
|
475
|
+
|
|
476
|
+
# GitHub token validation (if qualitative analysis enabled)
|
|
477
|
+
if form_data['enable_qualitative'] and not form_data['openrouter_key']:
|
|
478
|
+
errors.append("OpenRouter API key is required for qualitative analysis")
|
|
479
|
+
|
|
480
|
+
# Numeric field validation
|
|
481
|
+
try:
|
|
482
|
+
weeks = int(form_data['weeks'])
|
|
483
|
+
if weeks <= 0:
|
|
484
|
+
errors.append("Analysis period must be greater than 0")
|
|
485
|
+
except ValueError:
|
|
486
|
+
errors.append("Analysis period must be a valid number")
|
|
487
|
+
|
|
488
|
+
try:
|
|
489
|
+
threshold = float(form_data['similarity_threshold'])
|
|
490
|
+
if not 0.0 <= threshold <= 1.0:
|
|
491
|
+
errors.append("Similarity threshold must be between 0.0 and 1.0")
|
|
492
|
+
except ValueError:
|
|
493
|
+
errors.append("Similarity threshold must be a valid number")
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
ttl = int(form_data['cache_ttl'])
|
|
497
|
+
if ttl <= 0:
|
|
498
|
+
errors.append("Cache TTL must be greater than 0")
|
|
499
|
+
except ValueError:
|
|
500
|
+
errors.append("Cache TTL must be a valid number")
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
'valid': len(errors) == 0,
|
|
504
|
+
'errors': errors
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
def _show_validation_errors(self, errors: list) -> None:
|
|
508
|
+
"""Display validation errors to user."""
|
|
509
|
+
error_text = "Configuration validation failed:\\n" + "\\n".join(f"• {error}" for error in errors)
|
|
510
|
+
validation_msg = self.query_one("#validation-message", Static)
|
|
511
|
+
validation_msg.update(error_text)
|
|
512
|
+
validation_msg.add_class("validation-error")
|
|
513
|
+
validation_msg.remove_class("validation-success")
|
|
514
|
+
|
|
515
|
+
def _update_env_vars(self, form_data: Dict[str, Any]) -> None:
|
|
516
|
+
"""Update environment variables with provided values."""
|
|
517
|
+
env_updates = {}
|
|
518
|
+
|
|
519
|
+
if form_data['github_token']:
|
|
520
|
+
env_updates['GITHUB_TOKEN'] = form_data['github_token']
|
|
521
|
+
if form_data['github_org']:
|
|
522
|
+
env_updates['GITHUB_ORG'] = form_data['github_org']
|
|
523
|
+
if form_data['openrouter_key']:
|
|
524
|
+
env_updates['OPENROUTER_API_KEY'] = form_data['openrouter_key']
|
|
525
|
+
if form_data['jira_user']:
|
|
526
|
+
env_updates['JIRA_ACCESS_USER'] = form_data['jira_user']
|
|
527
|
+
if form_data['jira_token']:
|
|
528
|
+
env_updates['JIRA_ACCESS_TOKEN'] = form_data['jira_token']
|
|
529
|
+
|
|
530
|
+
# Update current environment
|
|
531
|
+
os.environ.update(env_updates)
|
|
532
|
+
|
|
533
|
+
def _create_config_from_form(self, form_data: Dict[str, Any]) -> Config:
|
|
534
|
+
"""
|
|
535
|
+
Create a Config object from form data.
|
|
536
|
+
|
|
537
|
+
WHY: Centralizes the logic for converting form inputs into a proper
|
|
538
|
+
configuration object, ensuring consistency and making it easier to
|
|
539
|
+
maintain as the configuration schema evolves.
|
|
540
|
+
"""
|
|
541
|
+
# This is a simplified version - in reality, you'd create a proper Config object
|
|
542
|
+
# with all the necessary structure and validation
|
|
543
|
+
|
|
544
|
+
# For now, return None and indicate this needs implementation
|
|
545
|
+
# TODO: Implement proper config creation from form data
|
|
546
|
+
self.notify("Configuration creation from form data not yet fully implemented", severity="warning")
|
|
547
|
+
return None
|