gitflow-analytics 1.0.1__py3-none-any.whl → 1.0.3__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 (57) hide show
  1. gitflow_analytics/__init__.py +11 -11
  2. gitflow_analytics/_version.py +2 -2
  3. gitflow_analytics/cli.py +612 -258
  4. gitflow_analytics/cli_rich.py +353 -0
  5. gitflow_analytics/config.py +251 -141
  6. gitflow_analytics/core/analyzer.py +140 -103
  7. gitflow_analytics/core/branch_mapper.py +132 -132
  8. gitflow_analytics/core/cache.py +240 -169
  9. gitflow_analytics/core/identity.py +210 -173
  10. gitflow_analytics/extractors/base.py +13 -11
  11. gitflow_analytics/extractors/story_points.py +70 -59
  12. gitflow_analytics/extractors/tickets.py +101 -87
  13. gitflow_analytics/integrations/github_integration.py +84 -77
  14. gitflow_analytics/integrations/jira_integration.py +116 -104
  15. gitflow_analytics/integrations/orchestrator.py +86 -85
  16. gitflow_analytics/metrics/dora.py +181 -177
  17. gitflow_analytics/models/database.py +190 -53
  18. gitflow_analytics/qualitative/__init__.py +30 -0
  19. gitflow_analytics/qualitative/classifiers/__init__.py +13 -0
  20. gitflow_analytics/qualitative/classifiers/change_type.py +468 -0
  21. gitflow_analytics/qualitative/classifiers/domain_classifier.py +399 -0
  22. gitflow_analytics/qualitative/classifiers/intent_analyzer.py +436 -0
  23. gitflow_analytics/qualitative/classifiers/risk_analyzer.py +412 -0
  24. gitflow_analytics/qualitative/core/__init__.py +13 -0
  25. gitflow_analytics/qualitative/core/llm_fallback.py +653 -0
  26. gitflow_analytics/qualitative/core/nlp_engine.py +373 -0
  27. gitflow_analytics/qualitative/core/pattern_cache.py +457 -0
  28. gitflow_analytics/qualitative/core/processor.py +540 -0
  29. gitflow_analytics/qualitative/models/__init__.py +25 -0
  30. gitflow_analytics/qualitative/models/schemas.py +272 -0
  31. gitflow_analytics/qualitative/utils/__init__.py +13 -0
  32. gitflow_analytics/qualitative/utils/batch_processor.py +326 -0
  33. gitflow_analytics/qualitative/utils/cost_tracker.py +343 -0
  34. gitflow_analytics/qualitative/utils/metrics.py +347 -0
  35. gitflow_analytics/qualitative/utils/text_processing.py +243 -0
  36. gitflow_analytics/reports/analytics_writer.py +11 -4
  37. gitflow_analytics/reports/csv_writer.py +51 -31
  38. gitflow_analytics/reports/narrative_writer.py +16 -14
  39. gitflow_analytics/tui/__init__.py +5 -0
  40. gitflow_analytics/tui/app.py +721 -0
  41. gitflow_analytics/tui/screens/__init__.py +8 -0
  42. gitflow_analytics/tui/screens/analysis_progress_screen.py +487 -0
  43. gitflow_analytics/tui/screens/configuration_screen.py +547 -0
  44. gitflow_analytics/tui/screens/loading_screen.py +358 -0
  45. gitflow_analytics/tui/screens/main_screen.py +304 -0
  46. gitflow_analytics/tui/screens/results_screen.py +698 -0
  47. gitflow_analytics/tui/widgets/__init__.py +7 -0
  48. gitflow_analytics/tui/widgets/data_table.py +257 -0
  49. gitflow_analytics/tui/widgets/export_modal.py +301 -0
  50. gitflow_analytics/tui/widgets/progress_widget.py +192 -0
  51. {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.0.3.dist-info}/METADATA +31 -4
  52. gitflow_analytics-1.0.3.dist-info/RECORD +62 -0
  53. gitflow_analytics-1.0.1.dist-info/RECORD +0 -31
  54. {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.0.3.dist-info}/WHEEL +0 -0
  55. {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.0.3.dist-info}/entry_points.txt +0 -0
  56. {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.0.3.dist-info}/licenses/LICENSE +0 -0
  57. {gitflow_analytics-1.0.1.dist-info → gitflow_analytics-1.0.3.dist-info}/top_level.txt +0 -0
@@ -1,289 +1,293 @@
1
1
  """DORA (DevOps Research and Assessment) metrics calculation."""
2
+
2
3
  from datetime import datetime
3
- from typing import Any, Dict, List
4
+ from typing import Any
4
5
 
5
6
  import numpy as np
6
7
 
7
8
 
8
9
  class DORAMetricsCalculator:
9
10
  """Calculate DORA metrics for software delivery performance."""
10
-
11
- def __init__(self):
11
+
12
+ def __init__(self) -> None:
12
13
  """Initialize DORA metrics calculator."""
13
- self.deployment_patterns = [
14
- 'deploy', 'release', 'ship', 'live', 'production', 'prod'
15
- ]
16
- self.failure_patterns = [
17
- 'revert', 'rollback', 'hotfix', 'emergency', 'incident', 'outage'
18
- ]
19
-
20
- def calculate_dora_metrics(self,
21
- commits: List[Dict[str, Any]],
22
- prs: List[Dict[str, Any]],
23
- start_date: datetime,
24
- end_date: datetime) -> Dict[str, Any]:
14
+ self.deployment_patterns = ["deploy", "release", "ship", "live", "production", "prod"]
15
+ self.failure_patterns = ["revert", "rollback", "hotfix", "emergency", "incident", "outage"]
16
+
17
+ def calculate_dora_metrics(
18
+ self,
19
+ commits: list[dict[str, Any]],
20
+ prs: list[dict[str, Any]],
21
+ start_date: datetime,
22
+ end_date: datetime,
23
+ ) -> dict[str, Any]:
25
24
  """Calculate the four key DORA metrics."""
26
-
25
+
27
26
  # Identify deployments and failures
28
27
  deployments = self._identify_deployments(commits, prs)
29
28
  failures = self._identify_failures(commits, prs)
30
-
29
+
31
30
  # Calculate metrics
32
31
  deployment_frequency = self._calculate_deployment_frequency(
33
32
  deployments, start_date, end_date
34
33
  )
35
-
34
+
36
35
  lead_time = self._calculate_lead_time(prs, deployments)
37
-
38
- change_failure_rate = self._calculate_change_failure_rate(
39
- deployments, failures
40
- )
41
-
36
+
37
+ change_failure_rate = self._calculate_change_failure_rate(deployments, failures)
38
+
42
39
  mttr = self._calculate_mttr(failures, commits)
43
-
40
+
44
41
  # Determine performance level
45
42
  performance_level = self._determine_performance_level(
46
43
  deployment_frequency, lead_time, change_failure_rate, mttr
47
44
  )
48
-
45
+
49
46
  return {
50
- 'deployment_frequency': deployment_frequency,
51
- 'lead_time_hours': lead_time,
52
- 'change_failure_rate': change_failure_rate,
53
- 'mttr_hours': mttr,
54
- 'performance_level': performance_level,
55
- 'total_deployments': len(deployments),
56
- 'total_failures': len(failures),
57
- 'metrics_period_weeks': (end_date - start_date).days / 7
47
+ "deployment_frequency": deployment_frequency,
48
+ "lead_time_hours": lead_time,
49
+ "change_failure_rate": change_failure_rate,
50
+ "mttr_hours": mttr,
51
+ "performance_level": performance_level,
52
+ "total_deployments": len(deployments),
53
+ "total_failures": len(failures),
54
+ "metrics_period_weeks": (end_date - start_date).days / 7,
58
55
  }
59
-
60
- def _identify_deployments(self, commits: List[Dict[str, Any]],
61
- prs: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
56
+
57
+ def _identify_deployments(
58
+ self, commits: list[dict[str, Any]], prs: list[dict[str, Any]]
59
+ ) -> list[dict[str, Any]]:
62
60
  """Identify deployment events from commits and PRs."""
63
61
  deployments = []
64
-
62
+
65
63
  # Check commits for deployment patterns
66
64
  for commit in commits:
67
- message_lower = commit['message'].lower()
65
+ message_lower = commit["message"].lower()
68
66
  if any(pattern in message_lower for pattern in self.deployment_patterns):
69
- deployments.append({
70
- 'type': 'commit',
71
- 'timestamp': commit['timestamp'],
72
- 'identifier': commit['hash'],
73
- 'message': commit['message']
74
- })
75
-
67
+ deployments.append(
68
+ {
69
+ "type": "commit",
70
+ "timestamp": commit["timestamp"],
71
+ "identifier": commit["hash"],
72
+ "message": commit["message"],
73
+ }
74
+ )
75
+
76
76
  # Check PR titles and labels for deployments
77
77
  for pr in prs:
78
78
  # Check title
79
- title_lower = pr.get('title', '').lower()
79
+ title_lower = pr.get("title", "").lower()
80
80
  if any(pattern in title_lower for pattern in self.deployment_patterns):
81
- deployments.append({
82
- 'type': 'pr',
83
- 'timestamp': pr.get('merged_at', pr.get('created_at')),
84
- 'identifier': f"PR#{pr['number']}",
85
- 'message': pr['title']
86
- })
81
+ deployments.append(
82
+ {
83
+ "type": "pr",
84
+ "timestamp": pr.get("merged_at", pr.get("created_at")),
85
+ "identifier": f"PR#{pr['number']}",
86
+ "message": pr["title"],
87
+ }
88
+ )
87
89
  continue
88
-
90
+
89
91
  # Check labels
90
- labels_lower = [label.lower() for label in pr.get('labels', [])]
91
- if any(any(pattern in label for pattern in self.deployment_patterns)
92
- for label in labels_lower):
93
- deployments.append({
94
- 'type': 'pr',
95
- 'timestamp': pr.get('merged_at', pr.get('created_at')),
96
- 'identifier': f"PR#{pr['number']}",
97
- 'message': pr['title']
98
- })
99
-
92
+ labels_lower = [label.lower() for label in pr.get("labels", [])]
93
+ if any(
94
+ any(pattern in label for pattern in self.deployment_patterns)
95
+ for label in labels_lower
96
+ ):
97
+ deployments.append(
98
+ {
99
+ "type": "pr",
100
+ "timestamp": pr.get("merged_at", pr.get("created_at")),
101
+ "identifier": f"PR#{pr['number']}",
102
+ "message": pr["title"],
103
+ }
104
+ )
105
+
100
106
  # Remove duplicates and sort by timestamp
101
107
  seen = set()
102
108
  unique_deployments = []
103
- for dep in sorted(deployments, key=lambda x: x['timestamp']):
109
+ for dep in sorted(deployments, key=lambda x: x["timestamp"]):
104
110
  key = f"{dep['type']}:{dep['identifier']}"
105
111
  if key not in seen:
106
112
  seen.add(key)
107
113
  unique_deployments.append(dep)
108
-
114
+
109
115
  return unique_deployments
110
-
111
- def _identify_failures(self, commits: List[Dict[str, Any]],
112
- prs: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
116
+
117
+ def _identify_failures(
118
+ self, commits: list[dict[str, Any]], prs: list[dict[str, Any]]
119
+ ) -> list[dict[str, Any]]:
113
120
  """Identify failure events from commits and PRs."""
114
121
  failures = []
115
-
122
+
116
123
  # Check commits for failure patterns
117
124
  for commit in commits:
118
- message_lower = commit['message'].lower()
125
+ message_lower = commit["message"].lower()
119
126
  if any(pattern in message_lower for pattern in self.failure_patterns):
120
- failures.append({
121
- 'type': 'commit',
122
- 'timestamp': commit['timestamp'],
123
- 'identifier': commit['hash'],
124
- 'message': commit['message'],
125
- 'is_hotfix': 'hotfix' in message_lower or 'emergency' in message_lower
126
- })
127
-
127
+ failures.append(
128
+ {
129
+ "type": "commit",
130
+ "timestamp": commit["timestamp"],
131
+ "identifier": commit["hash"],
132
+ "message": commit["message"],
133
+ "is_hotfix": "hotfix" in message_lower or "emergency" in message_lower,
134
+ }
135
+ )
136
+
128
137
  # Check PRs for failure patterns
129
138
  for pr in prs:
130
- title_lower = pr.get('title', '').lower()
131
- labels_lower = [label.lower() for label in pr.get('labels', [])]
132
-
133
- is_failure = (
134
- any(pattern in title_lower for pattern in self.failure_patterns) or
135
- any(any(pattern in label for pattern in self.failure_patterns)
136
- for label in labels_lower)
139
+ title_lower = pr.get("title", "").lower()
140
+ labels_lower = [label.lower() for label in pr.get("labels", [])]
141
+
142
+ is_failure = any(pattern in title_lower for pattern in self.failure_patterns) or any(
143
+ any(pattern in label for pattern in self.failure_patterns) for label in labels_lower
137
144
  )
138
-
145
+
139
146
  if is_failure:
140
- failures.append({
141
- 'type': 'pr',
142
- 'timestamp': pr.get('merged_at', pr.get('created_at')),
143
- 'identifier': f"PR#{pr['number']}",
144
- 'message': pr['title'],
145
- 'is_hotfix': 'hotfix' in title_lower or 'emergency' in title_lower
146
- })
147
-
147
+ failures.append(
148
+ {
149
+ "type": "pr",
150
+ "timestamp": pr.get("merged_at", pr.get("created_at")),
151
+ "identifier": f"PR#{pr['number']}",
152
+ "message": pr["title"],
153
+ "is_hotfix": "hotfix" in title_lower or "emergency" in title_lower,
154
+ }
155
+ )
156
+
148
157
  return failures
149
-
150
- def _calculate_deployment_frequency(self, deployments: List[Dict[str, Any]],
151
- start_date: datetime,
152
- end_date: datetime) -> Dict[str, Any]:
158
+
159
+ def _calculate_deployment_frequency(
160
+ self, deployments: list[dict[str, Any]], start_date: datetime, end_date: datetime
161
+ ) -> dict[str, Any]:
153
162
  """Calculate deployment frequency metrics."""
154
163
  if not deployments:
155
- return {
156
- 'daily_average': 0,
157
- 'weekly_average': 0,
158
- 'category': 'Low'
159
- }
160
-
164
+ return {"daily_average": 0, "weekly_average": 0, "category": "Low"}
165
+
161
166
  # Filter deployments in date range
162
- period_deployments = [
163
- d for d in deployments
164
- if start_date <= d['timestamp'] <= end_date
165
- ]
166
-
167
+ period_deployments = [d for d in deployments if start_date <= d["timestamp"] <= end_date]
168
+
167
169
  days = (end_date - start_date).days
168
170
  weeks = days / 7
169
-
171
+
170
172
  daily_avg = len(period_deployments) / days if days > 0 else 0
171
173
  weekly_avg = len(period_deployments) / weeks if weeks > 0 else 0
172
-
174
+
173
175
  # Categorize based on DORA standards
174
176
  if daily_avg >= 1:
175
- category = 'Elite' # Multiple deploys per day
177
+ category = "Elite" # Multiple deploys per day
176
178
  elif weekly_avg >= 1:
177
- category = 'High' # Between once per day and once per week
179
+ category = "High" # Between once per day and once per week
178
180
  elif weekly_avg >= 0.25:
179
- category = 'Medium' # Between once per week and once per month
181
+ category = "Medium" # Between once per week and once per month
180
182
  else:
181
- category = 'Low' # Less than once per month
182
-
183
- return {
184
- 'daily_average': daily_avg,
185
- 'weekly_average': weekly_avg,
186
- 'category': category
187
- }
188
-
189
- def _calculate_lead_time(self, prs: List[Dict[str, Any]],
190
- deployments: List[Dict[str, Any]]) -> float:
183
+ category = "Low" # Less than once per month
184
+
185
+ return {"daily_average": daily_avg, "weekly_average": weekly_avg, "category": category}
186
+
187
+ def _calculate_lead_time(
188
+ self, prs: list[dict[str, Any]], deployments: list[dict[str, Any]]
189
+ ) -> float:
191
190
  """Calculate lead time for changes in hours."""
192
191
  if not prs:
193
192
  return 0
194
-
193
+
195
194
  lead_times = []
196
-
195
+
197
196
  for pr in prs:
198
- if not pr.get('created_at') or not pr.get('merged_at'):
197
+ if not pr.get("created_at") or not pr.get("merged_at"):
199
198
  continue
200
-
199
+
201
200
  # Calculate time from PR creation to merge
202
- lead_time = (pr['merged_at'] - pr['created_at']).total_seconds() / 3600
201
+ lead_time = (pr["merged_at"] - pr["created_at"]).total_seconds() / 3600
203
202
  lead_times.append(lead_time)
204
-
203
+
205
204
  if not lead_times:
206
205
  return 0
207
-
206
+
208
207
  # Return median lead time
209
208
  return float(np.median(lead_times))
210
-
211
- def _calculate_change_failure_rate(self, deployments: List[Dict[str, Any]],
212
- failures: List[Dict[str, Any]]) -> float:
209
+
210
+ def _calculate_change_failure_rate(
211
+ self, deployments: list[dict[str, Any]], failures: list[dict[str, Any]]
212
+ ) -> float:
213
213
  """Calculate the percentage of deployments causing failures."""
214
214
  if not deployments:
215
215
  return 0
216
-
216
+
217
217
  # Count failures that occurred within 24 hours of a deployment
218
218
  failure_causing_deployments = 0
219
-
219
+
220
220
  for deployment in deployments:
221
- deploy_time = deployment['timestamp']
222
-
221
+ deploy_time = deployment["timestamp"]
222
+
223
223
  # Check if any failure occurred within 24 hours
224
224
  for failure in failures:
225
- failure_time = failure['timestamp']
225
+ failure_time = failure["timestamp"]
226
226
  time_diff = abs((failure_time - deploy_time).total_seconds() / 3600)
227
-
227
+
228
228
  if time_diff <= 24: # Within 24 hours
229
229
  failure_causing_deployments += 1
230
230
  break
231
-
231
+
232
232
  return (failure_causing_deployments / len(deployments)) * 100
233
-
234
- def _calculate_mttr(self, failures: List[Dict[str, Any]],
235
- commits: List[Dict[str, Any]]) -> float:
233
+
234
+ def _calculate_mttr(
235
+ self, failures: list[dict[str, Any]], commits: list[dict[str, Any]]
236
+ ) -> float:
236
237
  """Calculate mean time to recovery in hours."""
237
238
  if not failures:
238
239
  return 0
239
-
240
+
240
241
  recovery_times = []
241
-
242
+
242
243
  # For each failure, find the recovery time
243
- for i, failure in enumerate(failures):
244
- failure_time = failure['timestamp']
245
-
244
+ for _i, failure in enumerate(failures):
245
+ failure_time = failure["timestamp"]
246
+
246
247
  # Look for recovery indicators in subsequent commits
247
248
  recovery_time = None
248
-
249
+
249
250
  # Check subsequent commits for recovery patterns
250
251
  for commit in commits:
251
- if commit['timestamp'] <= failure_time:
252
+ if commit["timestamp"] <= failure_time:
252
253
  continue
253
-
254
- message_lower = commit['message'].lower()
255
- recovery_patterns = ['fixed', 'resolved', 'recovery', 'restored']
256
-
254
+
255
+ message_lower = commit["message"].lower()
256
+ recovery_patterns = ["fixed", "resolved", "recovery", "restored"]
257
+
257
258
  if any(pattern in message_lower for pattern in recovery_patterns):
258
- recovery_time = commit['timestamp']
259
+ recovery_time = commit["timestamp"]
259
260
  break
260
-
261
+
261
262
  # If we found a recovery, calculate MTTR
262
263
  if recovery_time:
263
264
  mttr = (recovery_time - failure_time).total_seconds() / 3600
264
265
  recovery_times.append(mttr)
265
266
  # For hotfixes, assume quick recovery (2 hours)
266
- elif failure.get('is_hotfix'):
267
+ elif failure.get("is_hotfix"):
267
268
  recovery_times.append(2.0)
268
-
269
+
269
270
  if not recovery_times:
270
271
  # If no explicit recovery found, estimate based on failure type
271
272
  return 4.0 # Default 4 hours
272
-
273
+
273
274
  return float(np.mean(recovery_times))
274
-
275
- def _determine_performance_level(self, deployment_freq: Dict[str, Any],
276
- lead_time_hours: float,
277
- change_failure_rate: float,
278
- mttr_hours: float) -> str:
275
+
276
+ def _determine_performance_level(
277
+ self,
278
+ deployment_freq: dict[str, Any],
279
+ lead_time_hours: float,
280
+ change_failure_rate: float,
281
+ mttr_hours: float,
282
+ ) -> str:
279
283
  """Determine overall performance level based on DORA metrics."""
280
284
  scores = []
281
-
285
+
282
286
  # Deployment frequency score
283
- freq_category = deployment_freq['category']
284
- freq_scores = {'Elite': 4, 'High': 3, 'Medium': 2, 'Low': 1}
287
+ freq_category = deployment_freq["category"]
288
+ freq_scores = {"Elite": 4, "High": 3, "Medium": 2, "Low": 1}
285
289
  scores.append(freq_scores.get(freq_category, 1))
286
-
290
+
287
291
  # Lead time score
288
292
  if lead_time_hours < 24: # Less than one day
289
293
  scores.append(4) # Elite
@@ -293,7 +297,7 @@ class DORAMetricsCalculator:
293
297
  scores.append(2) # Medium
294
298
  else:
295
299
  scores.append(1) # Low
296
-
300
+
297
301
  # Change failure rate score
298
302
  if change_failure_rate <= 15:
299
303
  scores.append(4) # Elite (0-15%)
@@ -303,7 +307,7 @@ class DORAMetricsCalculator:
303
307
  scores.append(2) # Medium
304
308
  else:
305
309
  scores.append(1) # Low
306
-
310
+
307
311
  # MTTR score
308
312
  if mttr_hours < 1: # Less than one hour
309
313
  scores.append(4) # Elite
@@ -313,15 +317,15 @@ class DORAMetricsCalculator:
313
317
  scores.append(2) # Medium
314
318
  else:
315
319
  scores.append(1) # Low
316
-
320
+
317
321
  # Average score determines overall level
318
322
  avg_score = sum(scores) / len(scores)
319
-
323
+
320
324
  if avg_score >= 3.5:
321
- return 'Elite'
325
+ return "Elite"
322
326
  elif avg_score >= 2.5:
323
- return 'High'
327
+ return "High"
324
328
  elif avg_score >= 1.5:
325
- return 'Medium'
329
+ return "Medium"
326
330
  else:
327
- return 'Low'
331
+ return "Low"