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.
- 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 +4108 -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 +904 -0
- gitflow_analytics/config/profiles.py +264 -0
- gitflow_analytics/config/repository.py +124 -0
- gitflow_analytics/config/schema.py +441 -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 +1193 -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.6.dist-info/METADATA +1015 -0
- gitflow_analytics-1.3.6.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.6.dist-info}/WHEEL +0 -0
- {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-1.0.3.dist-info → gitflow_analytics-1.3.6.dist-info}/top_level.txt +0 -0
gitflow_analytics/config.py
CHANGED
|
@@ -1,508 +1,44 @@
|
|
|
1
|
-
"""Configuration management for GitFlow Analytics.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
from
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
raise ValueError(f"Repository {repo_name} needs owner specified")
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
@dataclass
|
|
49
|
-
class AnalysisConfig:
|
|
50
|
-
"""Analysis-specific configuration."""
|
|
51
|
-
|
|
52
|
-
story_point_patterns: list[str] = field(default_factory=list)
|
|
53
|
-
exclude_authors: list[str] = field(default_factory=list)
|
|
54
|
-
exclude_message_patterns: list[str] = field(default_factory=list)
|
|
55
|
-
exclude_paths: list[str] = field(default_factory=list)
|
|
56
|
-
similarity_threshold: float = 0.85
|
|
57
|
-
manual_identity_mappings: list[dict[str, Any]] = field(default_factory=list)
|
|
58
|
-
default_ticket_platform: Optional[str] = None
|
|
59
|
-
branch_mapping_rules: dict[str, list[str]] = field(default_factory=dict)
|
|
60
|
-
ticket_platforms: Optional[list[str]] = None
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@dataclass
|
|
64
|
-
class OutputConfig:
|
|
65
|
-
"""Output configuration."""
|
|
66
|
-
|
|
67
|
-
directory: Optional[Path] = None
|
|
68
|
-
formats: list[str] = field(default_factory=lambda: ["csv", "markdown"])
|
|
69
|
-
csv_delimiter: str = ","
|
|
70
|
-
csv_encoding: str = "utf-8"
|
|
71
|
-
anonymize_enabled: bool = False
|
|
72
|
-
anonymize_fields: list[str] = field(default_factory=list)
|
|
73
|
-
anonymize_method: str = "hash"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
@dataclass
|
|
77
|
-
class CacheConfig:
|
|
78
|
-
"""Cache configuration."""
|
|
79
|
-
|
|
80
|
-
directory: Path = Path(".gitflow-cache")
|
|
81
|
-
ttl_hours: int = 168
|
|
82
|
-
max_size_mb: int = 500
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
@dataclass
|
|
86
|
-
class JIRAConfig:
|
|
87
|
-
"""JIRA configuration."""
|
|
88
|
-
|
|
89
|
-
access_user: str
|
|
90
|
-
access_token: str
|
|
91
|
-
base_url: Optional[str] = None
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
@dataclass
|
|
95
|
-
class JIRAIntegrationConfig:
|
|
96
|
-
"""JIRA integration specific configuration."""
|
|
97
|
-
|
|
98
|
-
enabled: bool = True
|
|
99
|
-
fetch_story_points: bool = True
|
|
100
|
-
project_keys: list[str] = field(default_factory=list)
|
|
101
|
-
story_point_fields: list[str] = field(
|
|
102
|
-
default_factory=lambda: ["customfield_10016", "customfield_10021", "Story Points"]
|
|
103
|
-
)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
@dataclass
|
|
107
|
-
class Config:
|
|
108
|
-
"""Main configuration container."""
|
|
109
|
-
|
|
110
|
-
repositories: list[RepositoryConfig]
|
|
111
|
-
github: GitHubConfig
|
|
112
|
-
analysis: AnalysisConfig
|
|
113
|
-
output: OutputConfig
|
|
114
|
-
cache: CacheConfig
|
|
115
|
-
jira: Optional[JIRAConfig] = None
|
|
116
|
-
jira_integration: Optional[JIRAIntegrationConfig] = None
|
|
117
|
-
qualitative: Optional['QualitativeConfig'] = None
|
|
118
|
-
|
|
119
|
-
def discover_organization_repositories(
|
|
120
|
-
self, clone_base_path: Optional[Path] = None
|
|
121
|
-
) -> list[RepositoryConfig]:
|
|
122
|
-
"""Discover repositories from GitHub organization.
|
|
123
|
-
|
|
124
|
-
Args:
|
|
125
|
-
clone_base_path: Base directory where repos should be cloned/found.
|
|
126
|
-
If None, uses output directory.
|
|
127
|
-
|
|
128
|
-
Returns:
|
|
129
|
-
List of discovered repository configurations.
|
|
130
|
-
"""
|
|
131
|
-
if not self.github.organization or not self.github.token:
|
|
132
|
-
return []
|
|
133
|
-
|
|
134
|
-
from github import Github
|
|
135
|
-
|
|
136
|
-
github_client = Github(self.github.token, base_url=self.github.base_url)
|
|
137
|
-
|
|
138
|
-
try:
|
|
139
|
-
org = github_client.get_organization(self.github.organization)
|
|
140
|
-
discovered_repos = []
|
|
141
|
-
|
|
142
|
-
base_path = clone_base_path or self.output.directory
|
|
143
|
-
if base_path is None:
|
|
144
|
-
raise ValueError("No base path available for repository cloning")
|
|
145
|
-
|
|
146
|
-
for repo in org.get_repos():
|
|
147
|
-
# Skip archived repositories
|
|
148
|
-
if repo.archived:
|
|
149
|
-
continue
|
|
150
|
-
|
|
151
|
-
# Create repository configuration
|
|
152
|
-
repo_path = base_path / repo.name
|
|
153
|
-
repo_config = RepositoryConfig(
|
|
154
|
-
name=repo.name,
|
|
155
|
-
path=repo_path,
|
|
156
|
-
github_repo=repo.full_name,
|
|
157
|
-
project_key=repo.name.upper().replace("-", "_"),
|
|
158
|
-
branch=repo.default_branch,
|
|
159
|
-
)
|
|
160
|
-
discovered_repos.append(repo_config)
|
|
161
|
-
|
|
162
|
-
return discovered_repos
|
|
163
|
-
|
|
164
|
-
except Exception as e:
|
|
165
|
-
raise ValueError(
|
|
166
|
-
f"Failed to discover repositories from organization {self.github.organization}: {e}"
|
|
167
|
-
) from e
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
class ConfigLoader:
|
|
171
|
-
"""Load and validate configuration from YAML files."""
|
|
172
|
-
|
|
173
|
-
@classmethod
|
|
174
|
-
def load(cls, config_path: Path) -> Config:
|
|
175
|
-
"""Load configuration from YAML file."""
|
|
176
|
-
# Load .env file from the same directory as the config file if it exists
|
|
177
|
-
config_dir = config_path.parent
|
|
178
|
-
env_file = config_dir / ".env"
|
|
179
|
-
if env_file.exists():
|
|
180
|
-
load_dotenv(env_file, override=True)
|
|
181
|
-
print(f"📋 Loaded environment variables from {env_file}")
|
|
182
|
-
|
|
183
|
-
with open(config_path) as f:
|
|
184
|
-
data = yaml.safe_load(f)
|
|
185
|
-
|
|
186
|
-
# Validate version
|
|
187
|
-
version = data.get("version", "1.0")
|
|
188
|
-
if version not in ["1.0"]:
|
|
189
|
-
raise ValueError(f"Unsupported config version: {version}")
|
|
190
|
-
|
|
191
|
-
# Process GitHub config
|
|
192
|
-
github_data = data.get("github", {})
|
|
193
|
-
|
|
194
|
-
# Resolve GitHub token
|
|
195
|
-
github_token = cls._resolve_env_var(github_data.get("token"))
|
|
196
|
-
if github_data.get("token") and not github_token:
|
|
197
|
-
raise ValueError(
|
|
198
|
-
"GitHub is configured but GITHUB_TOKEN environment variable is not set"
|
|
199
|
-
)
|
|
200
|
-
|
|
201
|
-
github_config = GitHubConfig(
|
|
202
|
-
token=github_token,
|
|
203
|
-
owner=cls._resolve_env_var(github_data.get("owner")),
|
|
204
|
-
organization=cls._resolve_env_var(github_data.get("organization")),
|
|
205
|
-
base_url=github_data.get("base_url", "https://api.github.com"),
|
|
206
|
-
max_retries=github_data.get("rate_limit", {}).get("max_retries", 3),
|
|
207
|
-
backoff_factor=github_data.get("rate_limit", {}).get("backoff_factor", 2),
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
# Process repositories
|
|
211
|
-
repositories = []
|
|
212
|
-
|
|
213
|
-
# Handle organization-based repository discovery
|
|
214
|
-
if github_config.organization and not data.get("repositories"):
|
|
215
|
-
# Organization specified but no explicit repositories - will be discovered at runtime
|
|
216
|
-
pass
|
|
217
|
-
else:
|
|
218
|
-
# Process explicitly defined repositories
|
|
219
|
-
for repo_data in data.get("repositories", []):
|
|
220
|
-
# Handle github_repo with owner/organization fallback
|
|
221
|
-
github_repo = repo_data.get("github_repo")
|
|
222
|
-
if github_repo and "/" not in github_repo:
|
|
223
|
-
if github_config.organization:
|
|
224
|
-
github_repo = f"{github_config.organization}/{github_repo}"
|
|
225
|
-
elif github_config.owner:
|
|
226
|
-
github_repo = f"{github_config.owner}/{github_repo}"
|
|
227
|
-
|
|
228
|
-
repo_config = RepositoryConfig(
|
|
229
|
-
name=repo_data["name"],
|
|
230
|
-
path=repo_data["path"],
|
|
231
|
-
github_repo=github_repo,
|
|
232
|
-
project_key=repo_data.get("project_key"),
|
|
233
|
-
branch=repo_data.get("branch"),
|
|
234
|
-
)
|
|
235
|
-
repositories.append(repo_config)
|
|
236
|
-
|
|
237
|
-
# Allow empty repositories list if organization is specified
|
|
238
|
-
if not repositories and not github_config.organization:
|
|
239
|
-
raise ValueError("No repositories defined and no organization specified for discovery")
|
|
240
|
-
|
|
241
|
-
# Process analysis settings
|
|
242
|
-
analysis_data = data.get("analysis", {})
|
|
243
|
-
|
|
244
|
-
# Default exclude paths for common boilerplate/generated files
|
|
245
|
-
default_exclude_paths = [
|
|
246
|
-
"**/node_modules/**",
|
|
247
|
-
"**/vendor/**",
|
|
248
|
-
"**/dist/**",
|
|
249
|
-
"**/build/**",
|
|
250
|
-
"**/.next/**",
|
|
251
|
-
"**/__pycache__/**",
|
|
252
|
-
"**/*.min.js",
|
|
253
|
-
"**/*.min.css",
|
|
254
|
-
"**/*.bundle.js",
|
|
255
|
-
"**/*.bundle.css",
|
|
256
|
-
"**/package-lock.json",
|
|
257
|
-
"**/yarn.lock",
|
|
258
|
-
"**/poetry.lock",
|
|
259
|
-
"**/Pipfile.lock",
|
|
260
|
-
"**/composer.lock",
|
|
261
|
-
"**/Gemfile.lock",
|
|
262
|
-
"**/Cargo.lock",
|
|
263
|
-
"**/go.sum",
|
|
264
|
-
"**/*.generated.*",
|
|
265
|
-
"**/generated/**",
|
|
266
|
-
"**/coverage/**",
|
|
267
|
-
"**/.coverage/**",
|
|
268
|
-
"**/htmlcov/**",
|
|
269
|
-
"**/*.map",
|
|
270
|
-
]
|
|
271
|
-
|
|
272
|
-
# Merge user-provided paths with defaults (user paths take precedence)
|
|
273
|
-
user_exclude_paths = analysis_data.get("exclude", {}).get("paths", [])
|
|
274
|
-
exclude_paths = user_exclude_paths if user_exclude_paths else default_exclude_paths
|
|
275
|
-
|
|
276
|
-
analysis_config = AnalysisConfig(
|
|
277
|
-
story_point_patterns=analysis_data.get(
|
|
278
|
-
"story_point_patterns",
|
|
279
|
-
[
|
|
280
|
-
r"(?:story\s*points?|sp|pts?)\s*[:=]\s*(\d+)",
|
|
281
|
-
r"\[(\d+)\s*(?:sp|pts?)\]",
|
|
282
|
-
r"#(\d+)sp",
|
|
283
|
-
],
|
|
284
|
-
),
|
|
285
|
-
exclude_authors=analysis_data.get("exclude", {}).get(
|
|
286
|
-
"authors", ["dependabot[bot]", "renovate[bot]"]
|
|
287
|
-
),
|
|
288
|
-
exclude_message_patterns=analysis_data.get("exclude", {}).get("message_patterns", []),
|
|
289
|
-
exclude_paths=exclude_paths,
|
|
290
|
-
similarity_threshold=analysis_data.get("identity", {}).get(
|
|
291
|
-
"similarity_threshold", 0.85
|
|
292
|
-
),
|
|
293
|
-
manual_identity_mappings=analysis_data.get("identity", {}).get("manual_mappings", []),
|
|
294
|
-
default_ticket_platform=analysis_data.get("default_ticket_platform"),
|
|
295
|
-
branch_mapping_rules=analysis_data.get("branch_mapping_rules", {}),
|
|
296
|
-
ticket_platforms=analysis_data.get("ticket_platforms"),
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
# Process output settings
|
|
300
|
-
output_data = data.get("output", {})
|
|
301
|
-
output_dir = output_data.get("directory")
|
|
302
|
-
if output_dir:
|
|
303
|
-
output_dir = Path(output_dir).expanduser()
|
|
304
|
-
# If relative path, make it relative to config file directory
|
|
305
|
-
if not output_dir.is_absolute():
|
|
306
|
-
output_dir = config_path.parent / output_dir
|
|
307
|
-
output_dir = output_dir.resolve()
|
|
308
|
-
else:
|
|
309
|
-
# Default to config file directory if not specified
|
|
310
|
-
output_dir = config_path.parent
|
|
311
|
-
|
|
312
|
-
output_config = OutputConfig(
|
|
313
|
-
directory=output_dir,
|
|
314
|
-
formats=output_data.get("formats", ["csv", "markdown"]),
|
|
315
|
-
csv_delimiter=output_data.get("csv", {}).get("delimiter", ","),
|
|
316
|
-
csv_encoding=output_data.get("csv", {}).get("encoding", "utf-8"),
|
|
317
|
-
anonymize_enabled=output_data.get("anonymization", {}).get("enabled", False),
|
|
318
|
-
anonymize_fields=output_data.get("anonymization", {}).get("fields", []),
|
|
319
|
-
anonymize_method=output_data.get("anonymization", {}).get("method", "hash"),
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
# Process cache settings
|
|
323
|
-
cache_data = data.get("cache", {})
|
|
324
|
-
cache_dir = cache_data.get("directory", ".gitflow-cache")
|
|
325
|
-
cache_path = Path(cache_dir)
|
|
326
|
-
# If relative path, make it relative to config file directory
|
|
327
|
-
if not cache_path.is_absolute():
|
|
328
|
-
cache_path = config_path.parent / cache_path
|
|
329
|
-
|
|
330
|
-
cache_config = CacheConfig(
|
|
331
|
-
directory=cache_path.resolve(),
|
|
332
|
-
ttl_hours=cache_data.get("ttl_hours", 168),
|
|
333
|
-
max_size_mb=cache_data.get("max_size_mb", 500),
|
|
334
|
-
)
|
|
335
|
-
|
|
336
|
-
# Process JIRA settings
|
|
337
|
-
jira_config = None
|
|
338
|
-
jira_data = data.get("jira", {})
|
|
339
|
-
if jira_data:
|
|
340
|
-
access_user = cls._resolve_env_var(jira_data.get("access_user", ""))
|
|
341
|
-
access_token = cls._resolve_env_var(jira_data.get("access_token", ""))
|
|
342
|
-
|
|
343
|
-
# Validate JIRA credentials if JIRA is configured
|
|
344
|
-
if jira_data.get("access_user") and jira_data.get("access_token"):
|
|
345
|
-
if not access_user:
|
|
346
|
-
raise ValueError(
|
|
347
|
-
"JIRA is configured but JIRA_ACCESS_USER environment variable is not set"
|
|
348
|
-
)
|
|
349
|
-
if not access_token:
|
|
350
|
-
raise ValueError(
|
|
351
|
-
"JIRA is configured but JIRA_ACCESS_TOKEN environment variable is not set"
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
jira_config = JIRAConfig(
|
|
355
|
-
access_user=access_user,
|
|
356
|
-
access_token=access_token,
|
|
357
|
-
base_url=jira_data.get("base_url"),
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
# Process JIRA integration settings
|
|
361
|
-
jira_integration_config = None
|
|
362
|
-
jira_integration_data = data.get("jira_integration", {})
|
|
363
|
-
if jira_integration_data:
|
|
364
|
-
jira_integration_config = JIRAIntegrationConfig(
|
|
365
|
-
enabled=jira_integration_data.get("enabled", True),
|
|
366
|
-
fetch_story_points=jira_integration_data.get("fetch_story_points", True),
|
|
367
|
-
project_keys=jira_integration_data.get("project_keys", []),
|
|
368
|
-
story_point_fields=jira_integration_data.get(
|
|
369
|
-
"story_point_fields", ["customfield_10016", "customfield_10021", "Story Points"]
|
|
370
|
-
),
|
|
371
|
-
)
|
|
372
|
-
|
|
373
|
-
# Process qualitative analysis settings
|
|
374
|
-
qualitative_config = None
|
|
375
|
-
qualitative_data = data.get("qualitative", {})
|
|
376
|
-
if qualitative_data:
|
|
377
|
-
# Import here to avoid circular imports
|
|
378
|
-
try:
|
|
379
|
-
from .qualitative.models.schemas import (
|
|
380
|
-
QualitativeConfig, NLPConfig, LLMConfig, CacheConfig as QualitativeCacheConfig,
|
|
381
|
-
ChangeTypeConfig, IntentConfig, DomainConfig, RiskConfig
|
|
382
|
-
)
|
|
383
|
-
|
|
384
|
-
# Parse NLP configuration
|
|
385
|
-
nlp_data = qualitative_data.get("nlp", {})
|
|
386
|
-
nlp_config = NLPConfig(
|
|
387
|
-
spacy_model=nlp_data.get("spacy_model", "en_core_web_sm"),
|
|
388
|
-
spacy_batch_size=nlp_data.get("spacy_batch_size", 1000),
|
|
389
|
-
fast_mode=nlp_data.get("fast_mode", True),
|
|
390
|
-
enable_parallel_processing=nlp_data.get("enable_parallel_processing", True),
|
|
391
|
-
max_workers=nlp_data.get("max_workers", 4),
|
|
392
|
-
change_type_config=ChangeTypeConfig(**nlp_data.get("change_type", {})),
|
|
393
|
-
intent_config=IntentConfig(**nlp_data.get("intent", {})),
|
|
394
|
-
domain_config=DomainConfig(**nlp_data.get("domain", {})),
|
|
395
|
-
risk_config=RiskConfig(**nlp_data.get("risk", {}))
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
# Parse LLM configuration
|
|
399
|
-
llm_data = qualitative_data.get("llm", {})
|
|
400
|
-
llm_config = LLMConfig(
|
|
401
|
-
openrouter_api_key=cls._resolve_env_var(llm_data.get("openrouter_api_key", "${OPENROUTER_API_KEY}")),
|
|
402
|
-
base_url=llm_data.get("base_url", "https://openrouter.ai/api/v1"),
|
|
403
|
-
primary_model=llm_data.get("primary_model", "anthropic/claude-3-haiku"),
|
|
404
|
-
fallback_model=llm_data.get("fallback_model", "meta-llama/llama-3.1-8b-instruct:free"),
|
|
405
|
-
complex_model=llm_data.get("complex_model", "anthropic/claude-3-sonnet"),
|
|
406
|
-
complexity_threshold=llm_data.get("complexity_threshold", 0.5),
|
|
407
|
-
cost_threshold_per_1k=llm_data.get("cost_threshold_per_1k", 0.01),
|
|
408
|
-
max_tokens=llm_data.get("max_tokens", 1000),
|
|
409
|
-
temperature=llm_data.get("temperature", 0.1),
|
|
410
|
-
max_group_size=llm_data.get("max_group_size", 10),
|
|
411
|
-
similarity_threshold=llm_data.get("similarity_threshold", 0.8),
|
|
412
|
-
requests_per_minute=llm_data.get("requests_per_minute", 200),
|
|
413
|
-
max_retries=llm_data.get("max_retries", 3),
|
|
414
|
-
max_daily_cost=llm_data.get("max_daily_cost", 5.0),
|
|
415
|
-
enable_cost_tracking=llm_data.get("enable_cost_tracking", True)
|
|
416
|
-
)
|
|
417
|
-
|
|
418
|
-
# Parse cache configuration
|
|
419
|
-
cache_data = qualitative_data.get("cache", {})
|
|
420
|
-
qualitative_cache_config = QualitativeCacheConfig(
|
|
421
|
-
cache_dir=cache_data.get("cache_dir", ".qualitative_cache"),
|
|
422
|
-
semantic_cache_size=cache_data.get("semantic_cache_size", 10000),
|
|
423
|
-
pattern_cache_ttl_hours=cache_data.get("pattern_cache_ttl_hours", 168),
|
|
424
|
-
enable_pattern_learning=cache_data.get("enable_pattern_learning", True),
|
|
425
|
-
learning_threshold=cache_data.get("learning_threshold", 10),
|
|
426
|
-
confidence_boost_factor=cache_data.get("confidence_boost_factor", 0.1),
|
|
427
|
-
enable_compression=cache_data.get("enable_compression", True),
|
|
428
|
-
max_cache_size_mb=cache_data.get("max_cache_size_mb", 100)
|
|
429
|
-
)
|
|
430
|
-
|
|
431
|
-
# Create main qualitative configuration
|
|
432
|
-
qualitative_config = QualitativeConfig(
|
|
433
|
-
enabled=qualitative_data.get("enabled", True),
|
|
434
|
-
batch_size=qualitative_data.get("batch_size", 1000),
|
|
435
|
-
max_llm_fallback_pct=qualitative_data.get("max_llm_fallback_pct", 0.15),
|
|
436
|
-
confidence_threshold=qualitative_data.get("confidence_threshold", 0.7),
|
|
437
|
-
nlp_config=nlp_config,
|
|
438
|
-
llm_config=llm_config,
|
|
439
|
-
cache_config=qualitative_cache_config,
|
|
440
|
-
enable_performance_tracking=qualitative_data.get("enable_performance_tracking", True),
|
|
441
|
-
target_processing_time_ms=qualitative_data.get("target_processing_time_ms", 2.0),
|
|
442
|
-
min_overall_confidence=qualitative_data.get("min_overall_confidence", 0.6),
|
|
443
|
-
enable_quality_feedback=qualitative_data.get("enable_quality_feedback", True)
|
|
444
|
-
)
|
|
445
|
-
|
|
446
|
-
except ImportError as e:
|
|
447
|
-
print(f"⚠️ Qualitative analysis dependencies missing: {e}")
|
|
448
|
-
print(" Install with: pip install spacy scikit-learn openai tiktoken")
|
|
449
|
-
qualitative_config = None
|
|
450
|
-
except Exception as e:
|
|
451
|
-
print(f"⚠️ Error parsing qualitative configuration: {e}")
|
|
452
|
-
qualitative_config = None
|
|
453
|
-
|
|
454
|
-
return Config(
|
|
455
|
-
repositories=repositories,
|
|
456
|
-
github=github_config,
|
|
457
|
-
analysis=analysis_config,
|
|
458
|
-
output=output_config,
|
|
459
|
-
cache=cache_config,
|
|
460
|
-
jira=jira_config,
|
|
461
|
-
jira_integration=jira_integration_config,
|
|
462
|
-
qualitative=qualitative_config,
|
|
463
|
-
)
|
|
464
|
-
|
|
465
|
-
@staticmethod
|
|
466
|
-
def _resolve_env_var(value: Optional[str]) -> Optional[str]:
|
|
467
|
-
"""Resolve environment variable references."""
|
|
468
|
-
if not value:
|
|
469
|
-
return None
|
|
470
|
-
|
|
471
|
-
if value.startswith("${") and value.endswith("}"):
|
|
472
|
-
env_var = value[2:-1]
|
|
473
|
-
resolved = os.environ.get(env_var)
|
|
474
|
-
if not resolved:
|
|
475
|
-
raise ValueError(f"Environment variable {env_var} not set")
|
|
476
|
-
return resolved
|
|
477
|
-
|
|
478
|
-
return value
|
|
479
|
-
|
|
480
|
-
@staticmethod
|
|
481
|
-
def validate_config(config: Config) -> list[str]:
|
|
482
|
-
"""Validate configuration and return list of warnings."""
|
|
483
|
-
warnings = []
|
|
484
|
-
|
|
485
|
-
# Check repository paths exist
|
|
486
|
-
for repo in config.repositories:
|
|
487
|
-
if not repo.path.exists():
|
|
488
|
-
warnings.append(f"Repository path does not exist: {repo.path}")
|
|
489
|
-
elif not (repo.path / ".git").exists():
|
|
490
|
-
warnings.append(f"Path is not a git repository: {repo.path}")
|
|
491
|
-
|
|
492
|
-
# Check GitHub token if GitHub repos are specified
|
|
493
|
-
has_github_repos = any(r.github_repo for r in config.repositories)
|
|
494
|
-
if has_github_repos and not config.github.token:
|
|
495
|
-
warnings.append("GitHub repositories specified but no GitHub token provided")
|
|
496
|
-
|
|
497
|
-
# Check if owner is needed
|
|
498
|
-
for repo in config.repositories:
|
|
499
|
-
if repo.github_repo and "/" not in repo.github_repo and not config.github.owner:
|
|
500
|
-
warnings.append(f"Repository {repo.github_repo} needs owner specified")
|
|
501
|
-
|
|
502
|
-
# Check cache directory permissions
|
|
503
|
-
try:
|
|
504
|
-
config.cache.directory.mkdir(exist_ok=True, parents=True)
|
|
505
|
-
except PermissionError:
|
|
506
|
-
warnings.append(f"Cannot create cache directory: {config.cache.directory}")
|
|
507
|
-
|
|
508
|
-
return warnings
|
|
1
|
+
"""Configuration management for GitFlow Analytics.
|
|
2
|
+
|
|
3
|
+
This module maintains backward compatibility by re-exporting all
|
|
4
|
+
configuration classes and the ConfigLoader from the refactored
|
|
5
|
+
config submodules.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Re-export everything from the new modular structure for backward compatibility
|
|
9
|
+
from .config import (
|
|
10
|
+
AnalysisConfig,
|
|
11
|
+
BranchAnalysisConfig,
|
|
12
|
+
CacheConfig,
|
|
13
|
+
CommitClassificationConfig,
|
|
14
|
+
Config,
|
|
15
|
+
ConfigLoader,
|
|
16
|
+
GitHubConfig,
|
|
17
|
+
JIRAConfig,
|
|
18
|
+
JIRAIntegrationConfig,
|
|
19
|
+
LLMClassificationConfig,
|
|
20
|
+
MLCategorization,
|
|
21
|
+
OutputConfig,
|
|
22
|
+
PMIntegrationConfig,
|
|
23
|
+
PMPlatformConfig,
|
|
24
|
+
RepositoryConfig,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Export all public interfaces
|
|
28
|
+
__all__ = [
|
|
29
|
+
"ConfigLoader",
|
|
30
|
+
"Config",
|
|
31
|
+
"RepositoryConfig",
|
|
32
|
+
"GitHubConfig",
|
|
33
|
+
"AnalysisConfig",
|
|
34
|
+
"OutputConfig",
|
|
35
|
+
"CacheConfig",
|
|
36
|
+
"JIRAConfig",
|
|
37
|
+
"JIRAIntegrationConfig",
|
|
38
|
+
"PMPlatformConfig",
|
|
39
|
+
"PMIntegrationConfig",
|
|
40
|
+
"MLCategorization",
|
|
41
|
+
"LLMClassificationConfig",
|
|
42
|
+
"CommitClassificationConfig",
|
|
43
|
+
"BranchAnalysisConfig",
|
|
44
|
+
]
|