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.
- gitflow_analytics/_version.py +1 -1
- gitflow_analytics/cli.py +164 -15
- gitflow_analytics/cli_wizards/__init__.py +10 -0
- gitflow_analytics/cli_wizards/install_wizard.py +936 -0
- gitflow_analytics/cli_wizards/run_launcher.py +343 -0
- gitflow_analytics/config/schema.py +12 -0
- gitflow_analytics/constants.py +75 -0
- gitflow_analytics/core/cache.py +7 -3
- gitflow_analytics/core/data_fetcher.py +66 -30
- gitflow_analytics/core/git_timeout_wrapper.py +6 -4
- gitflow_analytics/core/progress.py +2 -4
- gitflow_analytics/core/subprocess_git.py +31 -5
- gitflow_analytics/identity_llm/analysis_pass.py +13 -3
- gitflow_analytics/identity_llm/analyzer.py +14 -2
- gitflow_analytics/identity_llm/models.py +7 -1
- gitflow_analytics/qualitative/classifiers/llm/openai_client.py +5 -3
- gitflow_analytics/security/config.py +6 -6
- gitflow_analytics/security/extractors/dependency_checker.py +14 -14
- gitflow_analytics/security/extractors/secret_detector.py +8 -14
- gitflow_analytics/security/extractors/vulnerability_scanner.py +9 -9
- gitflow_analytics/security/llm_analyzer.py +10 -10
- gitflow_analytics/security/security_analyzer.py +17 -17
- gitflow_analytics/tui/screens/analysis_progress_screen.py +1 -1
- gitflow_analytics/ui/progress_display.py +36 -29
- gitflow_analytics/verify_activity.py +23 -26
- {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.4.7.dist-info}/METADATA +1 -1
- {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.4.7.dist-info}/RECORD +31 -29
- gitflow_analytics/security/reports/__init__.py +0 -5
- gitflow_analytics/security/reports/security_report.py +0 -358
- {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.4.7.dist-info}/WHEEL +0 -0
- {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.4.7.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-3.3.0.dist-info → gitflow_analytics-3.4.7.dist-info}/licenses/LICENSE +0 -0
- {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
|
gitflow_analytics/core/cache.py
CHANGED
|
@@ -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,
|
|
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"] >
|
|
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")
|