forked from greenelab/scrumlord
-
Notifications
You must be signed in to change notification settings - Fork 0
/
upkeep.py
158 lines (127 loc) · 4.14 KB
/
upkeep.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
154
155
156
157
158
import argparse
import datetime
import re
import holidays
import github
class PennHolidays(holidays.UnitedStates):
def _populate(self, year):
super()._populate(year)
# See https://github.com/greenelab/scrum/issues/114
for day in range(26, 32):
self[datetime.date(year, 12, day)] = 'Special Winter Vacation'
holiday_names = {
'Independence Day',
'Labor Day',
'Thanksgiving',
'Christmas Day',
"New Year's Day",
'Martin Luther King, Jr. Day',
'Memorial Day',
'Special Winter Vacation',
}
penn_holidays = PennHolidays()
def get_today():
"""
Returns the datetime.date for today. Needed since tests cannot mock a
builtin type: http://stackoverflow.com/a/24005764/4651668
"""
return datetime.date.today()
def is_holiday(date) -> bool:
"""
Return True or False for whether a date is a holiday
"""
name = penn_holidays.get(date)
if not name:
return False
name = name.replace(' (Observed)', '')
return name in holiday_names
def is_workday(date) -> bool:
"""
Return boolean for whether a date is a workday.
"""
if date.weekday() in holidays.WEEKEND:
return False
if is_holiday(date):
return False
return True
def issue_title_to_date(title: str):
"""
Return a datetime.date object from a Scrum issue title.
"""
pattern = re.compile(r'([0-9]{4})-([0-9]{2})-([0-9]{2}):')
match = pattern.match(title)
if not match:
return None
return datetime.date(*map(int, match.groups()))
def close_old_issues(issues, lifespan: int):
"""
Close scrum issues older than the number of days specified by lifespan.
"""
lifespan = datetime.timedelta(days=lifespan)
today = get_today()
for issue in issues:
title = issue.title
date = issue_title_to_date(title)
if not date:
continue
if today - date > lifespan:
print('Closing', title)
try:
issue.edit(state='closed')
except Exception as e:
print('Closing issue failed:', e)
def create_scrum_issue(repo, date):
"""
Create a scrum issue for the given date
"""
title = f"{date}: e-scrum for {date:%A, %B %-d, %Y}"
print('Creating', title)
try:
repo.create_issue(title)
except Exception as e:
print('Creating issue failed:', e)
def get_future_dates_without_issues(issues, workdays_ahead=2):
"""
Look through issues and yield the dates of future workdays (includes today)
that don't have open issues.
"""
future_dates = set(get_upcoming_workdays(workdays_ahead))
future_dates -= {issue_title_to_date(x.title) for x in issues}
return sorted(future_dates)
def get_upcoming_workdays(workdays_ahead=2):
"""
Return a generator of the next number of workdays specified by
workdays_ahead. The current day is yielded first, if a workday,
and does not count as one of workdays_ahead.
"""
date = get_today()
if is_workday(date):
yield date
i = 0
while i < workdays_ahead:
date += datetime.timedelta(days=1)
if is_workday(date):
yield date
i += 1
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--username', default='MarvinT')
parser.add_argument(
'--token', help='GitHub personal access token for --username')
parser.add_argument('--repository', default='gentnerlab/scrum')
parser.add_argument('--lifespan', type=int, default=7)
parser.add_argument('--workdays-ahead', type=int, default=2)
args = parser.parse_args()
gh = github.Github(args.username, args.token)
user = gh.get_user()
# Get greenelab/scrum repository. Could not find a better way
repo, = [repo for repo in user.get_repos()
if repo.full_name == args.repository]
# Get open issues
issues = list(repo.get_issues())
# Close old issues
close_old_issues(issues, args.lifespan)
# Create upcoming issues
dates = get_future_dates_without_issues(issues, args.workdays_ahead)
for date in dates:
create_scrum_issue(repo, date)