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
@@ -2,33 +2,30 @@
2
2
 
3
3
  import os
4
4
  from pathlib import Path
5
- from typing import Optional, Dict, Any
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 ConfigLoader, Config
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
- "Path where the configuration will be saved",
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=[Function(self._validate_positive_integer, "Must be a positive integer")]
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
- placeholder="your-organization",
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
- placeholder="your.email@company.com",
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=[Function(self._validate_positive_integer, "Must be a positive integer")]
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
- "Configure how repositories are discovered and analyzed",
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['valid']:
421
- self._show_validation_errors(validation_result['errors'])
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('config_path')
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) -> Dict[str, Any]:
421
+
422
+ def _collect_form_data(self) -> dict[str, Any]:
448
423
  """Collect all form data from inputs."""
449
424
  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,
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: Dict[str, Any]) -> Dict[str, Any]:
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['config_path']:
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['enable_qualitative'] and not form_data['openrouter_key']:
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['weeks'])
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['similarity_threshold'])
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['cache_ttl'])
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
- 'valid': len(errors) == 0,
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(f"• {error}" for error in errors)
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: Dict[str, Any]) -> None:
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['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
-
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: Dict[str, Any]) -> Config:
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("Configuration creation from form data not yet fully implemented", severity="warning")
547
- return None
520
+ self.notify(
521
+ "Configuration creation from form data not yet fully implemented", severity="warning"
522
+ )
523
+ return None