gitflow-analytics 1.0.0__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.
- gitflow_analytics/__init__.py +11 -9
- gitflow_analytics/_version.py +2 -2
- gitflow_analytics/cli.py +691 -243
- gitflow_analytics/cli_rich.py +353 -0
- gitflow_analytics/config.py +389 -96
- gitflow_analytics/core/analyzer.py +175 -78
- gitflow_analytics/core/branch_mapper.py +132 -132
- gitflow_analytics/core/cache.py +242 -173
- gitflow_analytics/core/identity.py +214 -178
- gitflow_analytics/extractors/base.py +13 -11
- gitflow_analytics/extractors/story_points.py +70 -59
- gitflow_analytics/extractors/tickets.py +111 -88
- gitflow_analytics/integrations/github_integration.py +91 -77
- gitflow_analytics/integrations/jira_integration.py +284 -0
- gitflow_analytics/integrations/orchestrator.py +99 -72
- gitflow_analytics/metrics/dora.py +183 -179
- gitflow_analytics/models/database.py +191 -54
- gitflow_analytics/qualitative/__init__.py +30 -0
- gitflow_analytics/qualitative/classifiers/__init__.py +13 -0
- gitflow_analytics/qualitative/classifiers/change_type.py +468 -0
- gitflow_analytics/qualitative/classifiers/domain_classifier.py +399 -0
- gitflow_analytics/qualitative/classifiers/intent_analyzer.py +436 -0
- gitflow_analytics/qualitative/classifiers/risk_analyzer.py +412 -0
- gitflow_analytics/qualitative/core/__init__.py +13 -0
- gitflow_analytics/qualitative/core/llm_fallback.py +653 -0
- gitflow_analytics/qualitative/core/nlp_engine.py +373 -0
- gitflow_analytics/qualitative/core/pattern_cache.py +457 -0
- gitflow_analytics/qualitative/core/processor.py +540 -0
- gitflow_analytics/qualitative/models/__init__.py +25 -0
- gitflow_analytics/qualitative/models/schemas.py +272 -0
- gitflow_analytics/qualitative/utils/__init__.py +13 -0
- gitflow_analytics/qualitative/utils/batch_processor.py +326 -0
- gitflow_analytics/qualitative/utils/cost_tracker.py +343 -0
- gitflow_analytics/qualitative/utils/metrics.py +347 -0
- gitflow_analytics/qualitative/utils/text_processing.py +243 -0
- gitflow_analytics/reports/analytics_writer.py +25 -8
- gitflow_analytics/reports/csv_writer.py +60 -32
- gitflow_analytics/reports/narrative_writer.py +21 -15
- gitflow_analytics/tui/__init__.py +5 -0
- gitflow_analytics/tui/app.py +721 -0
- gitflow_analytics/tui/screens/__init__.py +8 -0
- gitflow_analytics/tui/screens/analysis_progress_screen.py +487 -0
- gitflow_analytics/tui/screens/configuration_screen.py +547 -0
- gitflow_analytics/tui/screens/loading_screen.py +358 -0
- gitflow_analytics/tui/screens/main_screen.py +304 -0
- gitflow_analytics/tui/screens/results_screen.py +698 -0
- gitflow_analytics/tui/widgets/__init__.py +7 -0
- gitflow_analytics/tui/widgets/data_table.py +257 -0
- gitflow_analytics/tui/widgets/export_modal.py +301 -0
- gitflow_analytics/tui/widgets/progress_widget.py +192 -0
- gitflow_analytics-1.0.3.dist-info/METADATA +490 -0
- gitflow_analytics-1.0.3.dist-info/RECORD +62 -0
- gitflow_analytics-1.0.0.dist-info/METADATA +0 -201
- gitflow_analytics-1.0.0.dist-info/RECORD +0 -30
- {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/WHEEL +0 -0
- {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/entry_points.txt +0 -0
- {gitflow_analytics-1.0.0.dist-info → gitflow_analytics-1.0.3.dist-info}/licenses/LICENSE +0 -0
- {gitflow_analytics-1.0.0.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
|
-
|
|
3
|
-
from
|
|
4
|
-
from
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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(
|
|
61
|
-
|
|
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[
|
|
65
|
+
message_lower = commit["message"].lower()
|
|
68
66
|
if any(pattern in message_lower for pattern in self.deployment_patterns):
|
|
69
|
-
deployments.append(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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(
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
91
|
-
if any(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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[
|
|
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(
|
|
112
|
-
|
|
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[
|
|
125
|
+
message_lower = commit["message"].lower()
|
|
119
126
|
if any(pattern in message_lower for pattern in self.failure_patterns):
|
|
120
|
-
failures.append(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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(
|
|
131
|
-
labels_lower = [label.lower() for label in pr.get(
|
|
132
|
-
|
|
133
|
-
is_failure = (
|
|
134
|
-
any(pattern in
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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(
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
177
|
+
category = "Elite" # Multiple deploys per day
|
|
176
178
|
elif weekly_avg >= 1:
|
|
177
|
-
category =
|
|
179
|
+
category = "High" # Between once per day and once per week
|
|
178
180
|
elif weekly_avg >= 0.25:
|
|
179
|
-
category =
|
|
181
|
+
category = "Medium" # Between once per week and once per month
|
|
180
182
|
else:
|
|
181
|
-
category =
|
|
182
|
-
|
|
183
|
-
return {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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(
|
|
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[
|
|
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(
|
|
212
|
-
|
|
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[
|
|
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[
|
|
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(
|
|
235
|
-
|
|
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
|
|
244
|
-
failure_time = failure[
|
|
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[
|
|
252
|
+
if commit["timestamp"] <= failure_time:
|
|
252
253
|
continue
|
|
253
|
-
|
|
254
|
-
message_lower = commit[
|
|
255
|
-
recovery_patterns = [
|
|
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[
|
|
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(
|
|
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(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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[
|
|
284
|
-
freq_scores = {
|
|
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
|
|
325
|
+
return "Elite"
|
|
322
326
|
elif avg_score >= 2.5:
|
|
323
|
-
return
|
|
327
|
+
return "High"
|
|
324
328
|
elif avg_score >= 1.5:
|
|
325
|
-
return
|
|
329
|
+
return "Medium"
|
|
326
330
|
else:
|
|
327
|
-
return
|
|
331
|
+
return "Low"
|