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