gitflow-analytics 3.3.0__py3-none-any.whl → 3.4.7__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 (33) hide show
  1. gitflow_analytics/_version.py +1 -1
  2. gitflow_analytics/cli.py +164 -15
  3. gitflow_analytics/cli_wizards/__init__.py +10 -0
  4. gitflow_analytics/cli_wizards/install_wizard.py +936 -0
  5. gitflow_analytics/cli_wizards/run_launcher.py +343 -0
  6. gitflow_analytics/config/schema.py +12 -0
  7. gitflow_analytics/constants.py +75 -0
  8. gitflow_analytics/core/cache.py +7 -3
  9. gitflow_analytics/core/data_fetcher.py +66 -30
  10. gitflow_analytics/core/git_timeout_wrapper.py +6 -4
  11. gitflow_analytics/core/progress.py +2 -4
  12. gitflow_analytics/core/subprocess_git.py +31 -5
  13. gitflow_analytics/identity_llm/analysis_pass.py +13 -3
  14. gitflow_analytics/identity_llm/analyzer.py +14 -2
  15. gitflow_analytics/identity_llm/models.py +7 -1
  16. gitflow_analytics/qualitative/classifiers/llm/openai_client.py +5 -3
  17. gitflow_analytics/security/config.py +6 -6
  18. gitflow_analytics/security/extractors/dependency_checker.py +14 -14
  19. gitflow_analytics/security/extractors/secret_detector.py +8 -14
  20. gitflow_analytics/security/extractors/vulnerability_scanner.py +9 -9
  21. gitflow_analytics/security/llm_analyzer.py +10 -10
  22. gitflow_analytics/security/security_analyzer.py +17 -17
  23. gitflow_analytics/tui/screens/analysis_progress_screen.py +1 -1
  24. gitflow_analytics/ui/progress_display.py +36 -29
  25. gitflow_analytics/verify_activity.py +23 -26
  26. {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.4.7.dist-info}/METADATA +1 -1
  27. {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.4.7.dist-info}/RECORD +31 -29
  28. gitflow_analytics/security/reports/__init__.py +0 -5
  29. gitflow_analytics/security/reports/security_report.py +0 -358
  30. {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.4.7.dist-info}/WHEEL +0 -0
  31. {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.4.7.dist-info}/entry_points.txt +0 -0
  32. {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.4.7.dist-info}/licenses/LICENSE +0 -0
  33. {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.4.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,343 @@
1
+ """Interactive launcher for GitFlow Analytics with repository selection and preferences.
2
+
3
+ This module provides an interactive workflow for running GitFlow Analytics with:
4
+ - Configuration file selection
5
+ - Repository multi-select
6
+ - Analysis period configuration
7
+ - Cache management
8
+ - Persistent preferences
9
+ """
10
+
11
+ import logging
12
+ import subprocess
13
+ import sys
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+ from typing import Any, Optional
17
+
18
+ import click
19
+ import yaml
20
+
21
+ from ..config import Config, ConfigLoader
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class InteractiveLauncher:
27
+ """Interactive launcher for gitflow-analytics with preferences."""
28
+
29
+ def __init__(self, config_path: Optional[Path] = None):
30
+ """Initialize the interactive launcher.
31
+
32
+ Args:
33
+ config_path: Optional path to configuration file.
34
+ If not provided, will search for default configs.
35
+ """
36
+ self.config_path = config_path or self._find_default_config()
37
+ self.config: Optional[Config] = None
38
+ self.preferences: dict[str, Any] = {}
39
+
40
+ def run(self) -> bool:
41
+ """Execute interactive launcher workflow.
42
+
43
+ Returns:
44
+ True if analysis completed successfully, False otherwise.
45
+ """
46
+ click.echo("🚀 GitFlow Analytics Interactive Launcher\n")
47
+
48
+ # Step 1: Load configuration
49
+ if not self._load_config():
50
+ return False
51
+
52
+ # Step 2: Load existing preferences
53
+ self._load_preferences()
54
+
55
+ # Step 3: Repository selection
56
+ selected_repos = self._select_repositories()
57
+ if not selected_repos:
58
+ click.echo("❌ No repositories selected!")
59
+ return False
60
+
61
+ # Step 4: Analysis period
62
+ weeks = self._select_analysis_period()
63
+
64
+ # Step 5: Cache management
65
+ clear_cache = self._confirm_clear_cache()
66
+
67
+ # Step 6: Identity analysis option
68
+ skip_identity = self._confirm_skip_identity()
69
+
70
+ # Step 7: Save preferences
71
+ self._save_preferences(selected_repos, weeks, clear_cache, skip_identity)
72
+
73
+ # Step 8: Run analysis
74
+ return self._run_analysis(selected_repos, weeks, clear_cache, skip_identity)
75
+
76
+ def _find_default_config(self) -> Optional[Path]:
77
+ """Search for default configuration file.
78
+
79
+ Returns:
80
+ Path to config file if found, None otherwise.
81
+ """
82
+ # Common config file names in order of preference
83
+ config_names = [
84
+ "config.yaml",
85
+ "config.yml",
86
+ "gitflow-config.yaml",
87
+ "gitflow-config.yml",
88
+ ".gitflow.yaml",
89
+ ]
90
+
91
+ cwd = Path.cwd()
92
+ for name in config_names:
93
+ config_path = cwd / name
94
+ if config_path.exists():
95
+ return config_path
96
+
97
+ return None
98
+
99
+ def _load_config(self) -> bool:
100
+ """Load configuration file.
101
+
102
+ Returns:
103
+ True if configuration loaded successfully, False otherwise.
104
+ """
105
+ if not self.config_path:
106
+ click.echo("❌ No configuration file found!")
107
+ click.echo("\nSearched for: config.yaml, config.yml, gitflow-config.yaml")
108
+ click.echo("\n💡 Run 'gitflow-analytics install' to create a configuration")
109
+ return False
110
+
111
+ if not self.config_path.exists():
112
+ click.echo(f"❌ Configuration file not found: {self.config_path}")
113
+ return False
114
+
115
+ try:
116
+ click.echo(f"📁 Loading configuration from: {self.config_path}")
117
+ self.config = ConfigLoader.load(self.config_path)
118
+ click.echo("✅ Configuration loaded\n")
119
+ return True
120
+ except Exception as e:
121
+ click.echo(f"❌ Error loading configuration: {e}")
122
+ logger.error(f"Config loading error: {type(e).__name__}")
123
+ return False
124
+
125
+ def _load_preferences(self) -> None:
126
+ """Load existing launcher preferences from configuration."""
127
+ if not self.config:
128
+ return
129
+
130
+ # Load preferences from config YAML if they exist
131
+ try:
132
+ with open(self.config_path) as f:
133
+ config_data = yaml.safe_load(f)
134
+
135
+ if "launcher" in config_data:
136
+ self.preferences = config_data["launcher"]
137
+ logger.info(f"Loaded preferences: {self.preferences}")
138
+ except Exception as e:
139
+ logger.warning(f"Could not load preferences: {e}")
140
+ self.preferences = {}
141
+
142
+ def _select_repositories(self) -> list[str]:
143
+ """Interactive repository selection with multi-select.
144
+
145
+ Returns:
146
+ List of selected repository names.
147
+ """
148
+ if not self.config or not self.config.repositories:
149
+ click.echo("❌ No repositories configured!")
150
+ return []
151
+
152
+ click.echo("📂 Available Repositories:\n")
153
+
154
+ # Get last selected repos from preferences
155
+ last_selected = self.preferences.get("last_selected_repos", [])
156
+
157
+ # Display repositories with numbering and selection status
158
+ for i, repo in enumerate(self.config.repositories, 1):
159
+ repo_name = repo.path.name
160
+ status = "✓" if repo_name in last_selected else " "
161
+ click.echo(f" [{status}] {i}. {repo_name} ({repo.path})")
162
+
163
+ # Get user selection
164
+ click.echo("\n📝 Select repositories:")
165
+ click.echo(" • Enter numbers (comma-separated): 1,3,5")
166
+ click.echo(" • Enter 'all' for all repositories")
167
+ click.echo(" • Press Enter to use previous selection")
168
+
169
+ selection = click.prompt("Selection", default="", show_default=False).strip()
170
+
171
+ if selection.lower() == "all":
172
+ selected = [repo.path.name for repo in self.config.repositories]
173
+ click.echo(f"✅ Selected all {len(selected)} repositories\n")
174
+ return selected
175
+ elif not selection and last_selected:
176
+ click.echo(f"✅ Using previous selection: {len(last_selected)} repositories\n")
177
+ return last_selected
178
+ elif not selection:
179
+ # Default to all repos if no previous selection
180
+ selected = [repo.path.name for repo in self.config.repositories]
181
+ click.echo(f"✅ Selected all {len(selected)} repositories (default)\n")
182
+ return selected
183
+ else:
184
+ # Parse comma-separated numbers
185
+ try:
186
+ indices = [int(x.strip()) for x in selection.split(",")]
187
+ selected = []
188
+ for i in indices:
189
+ if 1 <= i <= len(self.config.repositories):
190
+ selected.append(self.config.repositories[i - 1].path.name)
191
+ else:
192
+ click.echo(f"⚠️ Invalid index: {i} (ignored)")
193
+
194
+ if not selected:
195
+ click.echo("❌ No valid repositories selected!")
196
+ return []
197
+
198
+ click.echo(f"✅ Selected {len(selected)} repositories\n")
199
+ return selected
200
+ except (ValueError, IndexError) as e:
201
+ click.echo(f"❌ Invalid selection: {e}")
202
+ return self._select_repositories() # Retry
203
+
204
+ def _select_analysis_period(self) -> int:
205
+ """Prompt for analysis period in weeks.
206
+
207
+ Returns:
208
+ Number of weeks to analyze.
209
+ """
210
+ default_weeks = self.preferences.get("default_weeks", 4)
211
+ weeks = click.prompt(
212
+ "📅 Number of weeks to analyze",
213
+ type=click.IntRange(1, 52),
214
+ default=default_weeks,
215
+ )
216
+ return weeks
217
+
218
+ def _confirm_clear_cache(self) -> bool:
219
+ """Confirm cache clearing.
220
+
221
+ Returns:
222
+ True if cache should be cleared, False otherwise.
223
+ """
224
+ default = self.preferences.get("auto_clear_cache", False)
225
+ return click.confirm("🗑️ Clear cache before analysis?", default=default)
226
+
227
+ def _confirm_skip_identity(self) -> bool:
228
+ """Confirm skipping identity analysis.
229
+
230
+ Returns:
231
+ True if identity analysis should be skipped, False otherwise.
232
+ """
233
+ default = self.preferences.get("skip_identity_analysis", False)
234
+ return click.confirm("🔍 Skip identity analysis?", default=default)
235
+
236
+ def _save_preferences(
237
+ self,
238
+ repos: list[str],
239
+ weeks: int,
240
+ clear_cache: bool,
241
+ skip_identity: bool,
242
+ ) -> None:
243
+ """Save preferences to config file.
244
+
245
+ Args:
246
+ repos: Selected repository names
247
+ weeks: Analysis period in weeks
248
+ clear_cache: Whether to clear cache
249
+ skip_identity: Whether to skip identity analysis
250
+ """
251
+ try:
252
+ click.echo("💾 Saving preferences...")
253
+
254
+ # Load existing config YAML
255
+ with open(self.config_path) as f:
256
+ config_data = yaml.safe_load(f)
257
+
258
+ # Update launcher preferences
259
+ config_data["launcher"] = {
260
+ "last_selected_repos": repos,
261
+ "default_weeks": weeks,
262
+ "auto_clear_cache": clear_cache,
263
+ "skip_identity_analysis": skip_identity,
264
+ "last_run": datetime.now(timezone.utc).isoformat(),
265
+ }
266
+
267
+ # Write back to file
268
+ with open(self.config_path, "w") as f:
269
+ yaml.dump(config_data, f, default_flow_style=False, sort_keys=False)
270
+
271
+ click.echo("✅ Preferences saved to config.yaml\n")
272
+ except Exception as e:
273
+ click.echo(f"⚠️ Could not save preferences: {e}")
274
+ logger.error(f"Preference saving error: {type(e).__name__}")
275
+
276
+ def _run_analysis(
277
+ self,
278
+ repos: list[str],
279
+ weeks: int,
280
+ clear_cache: bool,
281
+ skip_identity: bool,
282
+ ) -> bool:
283
+ """Execute analysis with selected options.
284
+
285
+ Args:
286
+ repos: Selected repository names
287
+ weeks: Analysis period in weeks
288
+ clear_cache: Whether to clear cache
289
+ skip_identity: Whether to skip identity analysis
290
+
291
+ Returns:
292
+ True if analysis completed successfully, False otherwise.
293
+ """
294
+ click.echo("🚀 Starting analysis...")
295
+ click.echo(f" Repositories: {', '.join(repos)}")
296
+ click.echo(f" Period: {weeks} weeks")
297
+ click.echo(f" Clear cache: {'Yes' if clear_cache else 'No'}")
298
+ click.echo(f" Skip identity: {'Yes' if skip_identity else 'No'}\n")
299
+
300
+ # Build command to execute
301
+ cmd = [
302
+ sys.executable,
303
+ "-m",
304
+ "gitflow_analytics.cli",
305
+ "analyze",
306
+ "-c",
307
+ str(self.config_path),
308
+ "--weeks",
309
+ str(weeks),
310
+ ]
311
+
312
+ if clear_cache:
313
+ cmd.append("--clear-cache")
314
+
315
+ if skip_identity:
316
+ cmd.append("--skip-identity-analysis")
317
+
318
+ # Execute analysis as subprocess to avoid Click context issues
319
+ try:
320
+ result = subprocess.run(cmd, check=True)
321
+ click.echo("\n✅ Analysis complete!")
322
+ return result.returncode == 0
323
+ except subprocess.CalledProcessError as e:
324
+ click.echo(f"\n❌ Analysis failed with exit code: {e.returncode}")
325
+ logger.error(f"Analysis subprocess error: {e}")
326
+ return False
327
+ except Exception as e:
328
+ click.echo(f"\n❌ Analysis failed: {e}")
329
+ logger.error(f"Analysis error: {type(e).__name__}")
330
+ return False
331
+
332
+
333
+ def run_interactive_launcher(config_path: Optional[Path] = None) -> bool:
334
+ """Run the interactive launcher.
335
+
336
+ Args:
337
+ config_path: Optional path to configuration file
338
+
339
+ Returns:
340
+ True if launcher completed successfully, False otherwise
341
+ """
342
+ launcher = InteractiveLauncher(config_path=config_path)
343
+ return launcher.run()
@@ -379,6 +379,17 @@ class PMIntegrationConfig:
379
379
  platforms: dict[str, PMPlatformConfig] = field(default_factory=dict)
380
380
 
381
381
 
382
+ @dataclass
383
+ class LauncherPreferences:
384
+ """Interactive launcher preferences."""
385
+
386
+ last_selected_repos: list[str] = field(default_factory=list)
387
+ default_weeks: int = 4
388
+ auto_clear_cache: bool = False
389
+ skip_identity_analysis: bool = False
390
+ last_run: Optional[str] = None
391
+
392
+
382
393
  @dataclass
383
394
  class Config:
384
395
  """Main configuration container."""
@@ -393,6 +404,7 @@ class Config:
393
404
  pm: Optional[Any] = None # Modern PM framework config
394
405
  pm_integration: Optional[PMIntegrationConfig] = None
395
406
  qualitative: Optional["QualitativeConfig"] = None
407
+ launcher: Optional[LauncherPreferences] = None
396
408
 
397
409
  def discover_organization_repositories(
398
410
  self, clone_base_path: Optional[Path] = None
@@ -0,0 +1,75 @@
1
+ """Application-wide constants and configuration values.
2
+
3
+ This module centralizes magic numbers and configuration defaults to improve
4
+ code maintainability and readability. Constants are organized by functional
5
+ area for easy navigation and updates.
6
+ """
7
+
8
+
9
+ class Timeouts:
10
+ """Timeout values in seconds for various git operations.
11
+
12
+ These timeouts protect against hanging operations when repositories
13
+ require authentication or have network issues.
14
+ """
15
+
16
+ # Git remote operations
17
+ GIT_FETCH = 30 # Fetch from remote repository
18
+ GIT_PULL = 30 # Pull latest changes
19
+
20
+ # Git local operations
21
+ GIT_BRANCH_ITERATION = 15 # Iterate commits for a branch/day
22
+ GIT_DIFF = 10 # Calculate diff statistics
23
+ GIT_CONFIG = 2 # Read git configuration
24
+ GIT_REMOTE_LIST = 5 # List remote branches
25
+
26
+ # Default timeout for generic git operations
27
+ DEFAULT_GIT_OPERATION = 30
28
+
29
+ # Process-level timeouts
30
+ SUBPROCESS_DEFAULT = 5 # Default subprocess timeout
31
+ THREAD_JOIN = 1 # Thread join timeout
32
+
33
+
34
+ class BatchSizes:
35
+ """Batch processing sizes for efficient data handling.
36
+
37
+ These sizes balance memory usage with performance gains from bulk operations.
38
+ Tunable based on repository size and system capabilities.
39
+ """
40
+
41
+ COMMIT_STORAGE = 1000 # Commits per bulk insert operation
42
+ TICKET_FETCH = 50 # Tickets fetched per JIRA batch
43
+ CACHE_WARMUP = 100 # Commits per cache warmup batch
44
+
45
+ # Estimation constants
46
+ COMMITS_PER_WEEK_ESTIMATE = 50 # Estimated commits for progress tracking
47
+ DEFAULT_PROGRESS_ESTIMATE = 100 # Default when estimation fails
48
+
49
+
50
+ class CacheTTL:
51
+ """Cache time-to-live values.
52
+
53
+ These values control how long cached data remains valid before
54
+ requiring refresh. Measured in hours unless otherwise specified.
55
+ """
56
+
57
+ ONE_WEEK_HOURS = 168 # Standard cache TTL (7 days * 24 hours)
58
+ IDENTITY_CACHE_DAYS = 7 # Developer identity analysis cache (in days)
59
+
60
+
61
+ class Thresholds:
62
+ """Various threshold values for analysis and reporting."""
63
+
64
+ # Cache performance
65
+ CACHE_HIT_RATE_GOOD = 50 # Percentage threshold for good cache performance
66
+
67
+ # Percentage calculations
68
+ PERCENTAGE_MULTIPLIER = 100 # Standard percentage calculation multiplier
69
+
70
+
71
+ class Estimations:
72
+ """Estimation constants for progress tracking and metrics."""
73
+
74
+ COMMITS_PER_WEEK = 50 # Estimated commits per week for progress bars
75
+ DEFAULT_ESTIMATE = 100 # Default estimate when actual count unavailable
@@ -12,6 +12,7 @@ from typing import Any, Optional, Union
12
12
  import git
13
13
  from sqlalchemy import and_
14
14
 
15
+ from ..constants import BatchSizes, CacheTTL, Thresholds
15
16
  from ..models.database import (
16
17
  CachedCommit,
17
18
  Database,
@@ -27,7 +28,10 @@ class GitAnalysisCache:
27
28
  """Cache for Git analysis results."""
28
29
 
29
30
  def __init__(
30
- self, cache_dir: Union[Path, str], ttl_hours: int = 168, batch_size: int = 1000
31
+ self,
32
+ cache_dir: Union[Path, str],
33
+ ttl_hours: int = CacheTTL.ONE_WEEK_HOURS,
34
+ batch_size: int = BatchSizes.COMMIT_STORAGE,
31
35
  ) -> None:
32
36
  """Initialize cache with SQLite backend and configurable batch size.
33
37
 
@@ -37,7 +41,7 @@ class GitAnalysisCache:
37
41
 
38
42
  Args:
39
43
  cache_dir: Directory for cache database
40
- ttl_hours: Time-to-live for cache entries in hours
44
+ ttl_hours: Time-to-live for cache entries in hours (default: 168 = 1 week)
41
45
  batch_size: Default batch size for bulk operations (default: 1000)
42
46
  """
43
47
  self.cache_dir = Path(cache_dir) # Ensure it's a Path object
@@ -643,7 +647,7 @@ class GitAnalysisCache:
643
647
  # Performance insights
644
648
  if stats["hit_rate_percent"] > 80:
645
649
  print(" ✅ Excellent cache performance!")
646
- elif stats["hit_rate_percent"] > 50:
650
+ elif stats["hit_rate_percent"] > Thresholds.CACHE_HIT_RATE_GOOD:
647
651
  print(" 👍 Good cache performance")
648
652
  elif stats["total_requests"] > 0:
649
653
  print(" ⚠️ Consider clearing stale cache entries")