-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.py
153 lines (128 loc) · 6.62 KB
/
main.py
1
2
3
4
5
6
7
8
9
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
from bs4 import BeautifulSoup
import math
# https://stackoverflow.com/questions/8595973/truncate-to-three-decimals-in-python
def truncate(number, digits) -> float:
stepper = 10.0 ** digits
return math.trunc(stepper * number) / stepper
StoryPoints = "series-field_customfield_10002-value"
StoryPointsDecrease = "series-field_customfield_10002-dec"
StoryPointsIncrease = "series-field_customfield_10002-inc"
ReportTemplate = '''
Started sprint with {} points
Finished sprint with {} points
We had {}% story point completion this sprint.
{} points were completed throughout the sprint.
{} points were added into the sprint after the beginning of the sprint.
{} points were removed from the sprint.
As a result of scope changes on tickets (pointed tickets increasing or decreasing in value), there were {} points added to the sprint.
If we were to take all scope changes and taking total points completed over it, we get {}% completed.
If we were to remove all scope changes and simply base completion on points completed / points at beginning: {}% completed
Any difference between actual points completed and beginning points can be attributed to scope changes in the middle of the sprint.
'''
def soupifyBurndown():
with open('burndown.html') as f:
burndownHtml = f.read()
soup = BeautifulSoup(burndownHtml, 'html.parser')
return soup
def getSprintStart(allTrs):
for tr in allTrs:
if tr.find(headers="series-event-type") and tr.find(headers="series-event-type").get_text() == "Sprint start":
return int(tr.find(headers=StoryPoints).get_text())
print("missing a sprint start value")
return 0
# make a guess since points rolling over apparently counts as "being added to the sprint".
def getSprintStartWithHeuristic(allTrs):
startValue = 0
for i, tr in enumerate(allTrs):
if tr.find(headers="series-event-type") and tr.find(headers="series-event-type").get_text() == "Sprint start":
startIdx = i
startValue = int(tr.find(headers=StoryPoints).get_text())
currIdx = startIdx + 1
tr = allTrs[currIdx]
while tr.find(headers="series-event-detail") and tr.find(headers="series-event-detail").get_text() == "Issue added to sprint":
if tr.find(headers=StoryPoints) and tr.find(headers=StoryPoints).get_text().isdigit():
startValue = int(tr.find(headers=StoryPoints).get_text())
currIdx += 1
tr = allTrs[currIdx]
return [startValue, currIdx]
def getSprintEnd(allTrs):
# maybe there's a "Sprint ended" column.
for i in range(len(allTrs) -1, -1, -1):
tr = allTrs[i]
if tr.find(headers="series-event-type") and 'Sprint ended by' in tr.find(headers="series-event-type").get_text():
allSps = tr.find(headers=StoryPoints)
return int(allSps.find_all('div')[-1].get_text())
# iterate from the last tr and try and find the "final" burndown value if sprint wasn't ended
for i in range(len(allTrs) -1, -1, -1):
tr = allTrs[i]
if tr.find(headers=StoryPoints) and tr.find(headers=StoryPoints).get_text().isdigit():
return int(tr.find(headers=StoryPoints).get_text())
print("missing a sprint end value")
return 0
def getPointsCompleted(allTrs):
totalCompleted = 0
for tr in allTrs:
if tr.find(headers="series-event-detail") and tr.find(headers="series-event-detail").get_text() == "Issue completed":
if tr.find(headers=StoryPointsDecrease) and tr.find(headers=StoryPointsDecrease).get_text().isdigit():
totalCompleted += int(tr.find(headers=StoryPointsDecrease).get_text())
return totalCompleted
def getPointsAdded(allTrs, idx = 0):
totalAdded = 0
for tr in allTrs[idx:]:
if tr.find(headers="series-event-detail") and tr.find(headers="series-event-detail").get_text() == "Issue added to sprint":
if tr.find(headers=StoryPointsIncrease) and tr.find(headers=StoryPointsIncrease).get_text().isdigit():
totalAdded += int(tr.find(headers=StoryPointsIncrease).get_text())
return totalAdded
def getPointsRemoved(allTrs):
totalRemoved = 0
for tr in allTrs:
if tr.find(headers="series-event-detail") and tr.find(headers="series-event-detail").get_text() == "Issue removed from sprint":
if tr.find(headers=StoryPointsDecrease) and tr.find(headers=StoryPointsDecrease).get_text().isdigit():
totalRemoved += int(tr.find(headers=StoryPointsDecrease).get_text())
return totalRemoved
def getScopeChanges(allTrs):
totalScopeChange = 0
for tr in allTrs:
if tr.find(headers="series-event-type") and tr.find(headers="series-event-type").get_text() == "Scope change":
if tr.find(headers="series-event-detail") and 'Estimate' in tr.find(headers="series-event-detail").get_text():
if tr.find(headers=StoryPointsDecrease) and tr.find(headers=StoryPointsDecrease).get_text().isdigit():
totalScopeChange -= int(tr.find(headers=StoryPointsDecrease).get_text())
elif tr.find(headers=StoryPointsIncrease) and tr.find(headers=StoryPointsIncrease).get_text().isdigit():
totalScopeChange += int(tr.find(headers=StoryPointsIncrease).get_text())
return totalScopeChange
def main():
soup = soupifyBurndown()
allTr = soup.find_all('tr')
startingStoryPoints = getSprintStart(allTr)
endingStoryPoints = getSprintEnd(allTr)
totalCompleted = getPointsCompleted(allTr)
pointsAdded = getPointsAdded(allTr)
pointsRemoved = getPointsRemoved(allTr)
scopeChanges = getScopeChanges(allTr)
totalPoints = startingStoryPoints - pointsRemoved + pointsAdded + scopeChanges
print('Without applying heuristic:', ReportTemplate.format(
startingStoryPoints,
endingStoryPoints,
truncate((startingStoryPoints - endingStoryPoints)/startingStoryPoints, 2) * 100,
totalCompleted,
pointsAdded,
pointsRemoved,
scopeChanges,
truncate(totalCompleted/totalPoints, 2)*100,
truncate(totalCompleted/startingStoryPoints, 2)*100
))
heuristicStart, endIdx = getSprintStartWithHeuristic(allTr)
pointsAdded = getPointsAdded(allTr, endIdx)
print('If applying heuristic:', ReportTemplate.format(
heuristicStart,
endingStoryPoints,
truncate((heuristicStart - endingStoryPoints)/heuristicStart, 2) * 100,
totalCompleted,
pointsAdded,
pointsRemoved,
scopeChanges,
truncate(totalCompleted/totalPoints, 2)*100,
truncate(totalCompleted/heuristicStart, 2)*100,
))
if __name__ == '__main__':
main()