Skip to content

Commit 4d3ca0c

Browse files
committed
Add a script for generating spreadsheets of issues closed in sprints
1 parent ec9e7fc commit 4d3ca0c

File tree

1 file changed

+144
-0
lines changed

1 file changed

+144
-0
lines changed

make-sprint-milestone-spreadsheets.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#!/usr/bin/env python
2+
3+
from collections import defaultdict
4+
import csv
5+
import datetime
6+
import dateutil.parser
7+
import doctest
8+
from optparse import OptionParser
9+
import os
10+
from os.path import dirname, join, realpath
11+
import re
12+
import requests
13+
14+
from github import standard_headers, get_issues
15+
16+
# If you use milestones to group the issues that you hope to close
17+
# over the course of the sprint, and 'Difficulty N' labels to indicate
18+
# their relative complexity, this script will generate CSV files with
19+
# the issues for each sprint.
20+
21+
cwd = os.getcwd()
22+
repo_directory = realpath(join(dirname(__file__)))
23+
24+
def get_difficulty(issue):
25+
difficulty_label_names = [i['name'] for i in issue['labels']
26+
if re.search(r'^Difficulty ', i['name'])]
27+
if len(difficulty_label_names) == 0:
28+
return None
29+
elif len(difficulty_label_names) == 1:
30+
label = difficulty_label_names[0]
31+
m = re.search(r'^Difficulty (\d+)', label)
32+
if not m:
33+
message = "Malformed Difficulty label: '{0}'".format(label)
34+
raise Exception, message
35+
return int(m.group(1), 10)
36+
else:
37+
raise Exception, "Found multiple Difficulty labels: '{0}'".format(
38+
difficulty_label_names)
39+
40+
def issues_sort_key(issue):
41+
state_number = 0 if issue['state'] == 'closed' else 1
42+
closed_at = issue['closed_at']
43+
return (state_number, closed_at)
44+
45+
def main(repo):
46+
47+
milestone_due_dates = {}
48+
milestone_start_dates = {}
49+
50+
for milestone_status in ('open', 'closed'):
51+
milestones_url = 'https://api.github.com/repos/{0}/milestones'.format(repo)
52+
r = requests.get(milestones_url,
53+
params={'state': 'open'},
54+
headers=standard_headers)
55+
if r.status_code != 200:
56+
raise Exception, "HTTP status {0} on fetching {1}".format(
57+
r.status_code,
58+
milestones_url)
59+
for milestone in r.json():
60+
title = milestone['title']
61+
if title.startswith('Sprint'):
62+
due_date = dateutil.parser.parse(milestone['due_on']) + \
63+
datetime.timedelta(hours=4)
64+
start_date = due_date - datetime.timedelta(days=7)
65+
milestone_start_dates[title] = start_date
66+
milestone_due_dates[title] = due_date
67+
68+
milestones = sorted(milestone_due_dates.keys())
69+
70+
issues_in_milestone = defaultdict(list)
71+
issues_not_in_milestone = defaultdict(list)
72+
73+
for number, title, body, issue in get_issues(repo, state='all'):
74+
75+
if ('pull_request' in issue) and issue['pull_request']['html_url']:
76+
continue
77+
78+
issue['difficulty'] = get_difficulty(issue)
79+
80+
print "considering issue:", number
81+
82+
date_closed = issue['closed_at']
83+
if date_closed is not None:
84+
date_closed = dateutil.parser.parse(date_closed)
85+
86+
issue['closed_at'] = date_closed
87+
88+
milestone = issue['milestone']
89+
if milestone is not None:
90+
milestone = milestone['title']
91+
92+
if milestone in milestone_due_dates:
93+
issues_in_milestone[milestone].append(issue)
94+
elif date_closed:
95+
for possible_milestone in milestones:
96+
if date_closed > milestone_start_dates[possible_milestone] and \
97+
date_closed <= milestone_due_dates[possible_milestone]:
98+
issues_not_in_milestone[possible_milestone].append(issue)
99+
100+
for milestone in milestones:
101+
102+
filename = milestone + ".csv"
103+
104+
print "writing:", filename
105+
106+
with open(filename, "w") as fp:
107+
writer = csv.writer(fp)
108+
writer.writerow(['OfficiallyInMilestone', 'State', 'URL', 'Number', 'Difficulty', 'Title'])
109+
in_milestone = sorted(issues_in_milestone[milestone],
110+
key=issues_sort_key)
111+
not_in_milestone = sorted(issues_not_in_milestone[milestone],
112+
key=issues_sort_key)
113+
for issue in in_milestone:
114+
writer.writerow([True,
115+
issue['state'],
116+
issue['html_url'],
117+
issue['number'],
118+
issue['difficulty'],
119+
issue['title']])
120+
for issue in not_in_milestone:
121+
writer.writerow([False,
122+
issue['state'],
123+
issue['html_url'],
124+
issue['number'],
125+
issue['difficulty'],
126+
issue['title']])
127+
128+
usage = """Usage: %prog [options] REPOSITORY
129+
130+
Repository should be username/repository from GitHub, e.g. mysociety/pombola"""
131+
parser = OptionParser(usage=usage)
132+
parser.add_option("-t", "--test",
133+
action="store_true", dest="test", default=False,
134+
help="Run doctests")
135+
136+
(options, args) = parser.parse_args()
137+
138+
if options.test:
139+
doctest.testmod()
140+
else:
141+
if len(args) != 1:
142+
parser.print_help()
143+
else:
144+
main(args[0])

0 commit comments

Comments
 (0)