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