-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithub-teamactivity.py
More file actions
203 lines (174 loc) · 8.65 KB
/
Copy pathgithub-teamactivity.py
File metadata and controls
203 lines (174 loc) · 8.65 KB
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
''' Generates a graph showing Github team contributions for specified team(s) and
# date range.'''
import os
import time
import calendar
from datetime import datetime, timezone, date, timedelta
import matplotlib.pyplot as plt
import matplotlib.patheffects as PathEffects
import pandas as pd
from github import Github
def api_wait_search(git, min_srchs=4):
''' Automatic rate limiter '''
limits = git.get_rate_limit()
if limits.search.remaining <= min_srchs:
local_reset = limits.search.reset.replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
seconds = round((local_reset - now).total_seconds()) + 20 # add 20 secs as GH isn't precise
print(f'API rate limit exceeded - waiting for {seconds} seconds...')
time.sleep(seconds)
print("Done waiting - resuming.")
def get_AC_team(git, targets='triage'):
''' Given a team designation, return names of all members'''
# AC ID = 20147732, Bug Triaging Team ID = 4914022, Senior BT = 4916549
# testers = 2167099, devs = 2059572
# Multiple teams can be merged and treated as one unit, i.e. triage/senior triage
# Can also manually specify team members, where no formal team in Github exists.
teamnames = []
org = git.get_organization('azerothcore')
id_dict = {'triage': [4914022, 4916549], 'alldevs': [2059572],
'testers': [2167099]}
if targets in id_dict.keys():
team_ids = id_dict[targets]
else: # no team id, directly specify team names instead
teamnames = ['UltraNix', 'IntelligentQuantum', 'Nyeriah', 'Nefertumm',
'Winfidonarleyan']
team_ids = []
for team_id in team_ids:
team = org.get_team(team_id)
# just gets first page of names, no team is larger than that so isn't a problem
teamlist = team.get_members().get_page(0)
teamnames += [x.login for x in teamlist]
return sorted(list(set(teamnames)), key = lambda x:x.capitalize(), reverse=True)
def convert_data(data, targets):
newdata = {'Name':list(data.keys())}
if targets == 'triage':
headings = ['AC Issues Created', 'CC Issues Involved', 'PRs Created'] # 'AC Other',
elif targets in ['selectdevs', 'alldevs']:
headings = ['PRs Made', 'PRs Reviewed']
elif targets == 'testers':
headings = ['PRs Involved', 'PRs Made']
for num, k in enumerate(headings):
newdata[k] = [x[num] for x in data.values()]
return newdata
def generate_stackbar(data, targets, datemode, datevar):
titles = {'triage': 'Triaging Team', 'testers': 'Testing Team',
'selectdevs': 'Selected Developers', 'alldevs': 'All Developers'}
data = convert_data(data, targets)
df = pd.DataFrame(data)
df.set_index('Name', inplace=True)
graphtitles = {'daysback': f'Last {datevar} Days',
'year': f'Year {datevar}',
'month': f'{calendar.month_name[datevar] if 1 <= datevar <= 12 else "x"}'}
graphtitle = f'AC {titles[targets]} Activity For ' + graphtitles[datemode]
filename = graphtitle + f' - {date.today().isoformat()}.png'
font_color = 'black'
hfont = {'fontname':'Calibri'}
figsize_y = max(len(df) * 0.5, 7.5) # dynamic resize
ax = df.plot(kind='barh', stacked=True, edgecolor='black', linewidth=1,
width=0.75, figsize=(12, figsize_y), color=[u'#1f77b4', u'#2ca02c', u'#d62728']) #
for label in ax.get_xticklabels() + ax.get_yticklabels():
label.set_fontsize(14) #16
plt.xticks(color=font_color, **hfont)
plt.yticks(color=font_color, **hfont)
for p in ax.patches:
width, height = p.get_width(), p.get_height()
x, y = p.get_xy()
bar_fontsize = 18 if len(df) < 10 else 15 #resize font if more bars
if width:
text = ax.text(x+width/2, y+height/2, '{:.0f}'.format(width),
horizontalalignment='center', verticalalignment='center',
color='white', fontsize=bar_fontsize, **hfont)
text.set_path_effects([PathEffects.withStroke(linewidth=2, foreground='black')])
title = plt.title(f'{graphtitle}', fontsize=16, color=font_color, **hfont)
ax.legend()
plt.tight_layout()
plt.savefig(f'{filename}', dpi=100)
plt.show()
def generate_searchstrs(contrib, datemode='daysback', datevar=30, targets='triage'):
lastdays = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
srchstrs = []
curryear, currmonth = time.localtime()[0], time.localtime()[1]
if datemode == 'month' and currmonth < datevar: #month in previous year
curryear -= 1
if datemode == 'daysback':
days_before = (date.today() - timedelta(days=datevar)).isoformat()
datestr = f'created:>={days_before}'
elif datemode == 'month':
lastday = lastdays[datevar]
monthvar = str(datevar).zfill(2)
datestr = f'created:{curryear}-{monthvar}-01..{curryear}-{monthvar}-{lastday}'
elif datemode == 'year':
datestr = f'created:{datevar}-01-01..{datevar}-12-31'
if targets == 'triage':
srchstrs = [f'is:issue {datestr} author:{contrib} org:azerothcore',
f'is:issue {datestr} involves:{contrib} repo:chromiecraft/chromiecraft',
f'is:pr {datestr} org:azerothcore author:{contrib}',
f'is:issue {datestr} author:{contrib} repo:chromiecraft/chromiecraft']
elif targets in ['selectdevs', 'alldevs']:
srchstrs = [f'is:pr {datestr} org:azerothcore author:{contrib}',
f'is:pr {datestr} org:azerothcore reviewed-by:{contrib}']
elif targets == 'testers': # not really useful as performance metrics, ask Temper
srchstrs = [f'is:pr {datestr} involves:{contrib} org:azerothcore',
f'is:pr {datestr} author:{contrib} org:azerothcore']
return srchstrs
def scan_contribs(git, targets='triage', datemode='daysback', datevar=30):
'''Given a team designation and a date range, retrieve stats for team members'''
teamlist = get_AC_team(git, targets)
cont_data = {k:[] for k in teamlist}
for contrib in teamlist:
print(f'Scanning contributions for {contrib}...')
srchstrs = generate_searchstrs(contrib, datemode, datevar, targets)
api_wait_search(git, len(srchstrs))
for srch in srchstrs:
while True:
try:
results = git.search_issues(srch)
if results.totalCount == 1000: # workaround for API 1K search limit
results.get_page(0)
cont_data[contrib].append(results.totalCount)
if results:
break
except Exception as err:
print(f'Error: {err}. Pausing for 20 seconds.')
time.sleep(20)
time.sleep(4)
if targets == 'triage': # subtract authored CC issues from involved issues
for k, v in cont_data.items():
cont_data[k] = [v[0], max(v[1] - v[3], 0), v[2]]
if targets == 'testers': # subtract authored AC PRs from involved PRs
for k, v in cont_data.items():
cont_data[k] = [v[0] - v[1], v[1]]
if targets != 'selectdevs': # remove null contributors from all but selected devs
cont_data = {k:v for k, v in cont_data.items() if sum([1 for x in v if not x]) != len(v)}
totals = []
for p in range(len(list(cont_data.values())[0])):
totals.append(sum([v[p] for v in cont_data.values()]))
if targets == 'triage':
print(f'AC total created: {totals[0]}, CC involved: '\
f'{totals[1]}, PRs made: {totals[2]}.')
elif targets in ['selectdevs', 'alldevs']:
print(f'PRs made: {totals[0]}, PRs reviewed: {totals[1]}')
# can clear a specified team member's stats here
if 'Azcobu' in cont_data:
del cont_data['Azcobu']
print(cont_data)
return cont_data
def main():
# Time modes: daysback = last X days, month = activity in the listed month of this year,
# year = activity for that year. If not specified, defaults to daysback.
datemode = 'month'
# datevar: if time mode is dayback, datevar is number of days to go back. (default = 30)
# if time mode is month, datevar is number of month, i.e. May = 5
# if time mode is year, datevar is the year.
datevar = 12
# Targets: which team or people to gather stats on.
# Options are 'triage', 'selectdevs', 'alldevs', 'testers'. Default is triage.
targets = 'triage'
if token := os.getenv('GITHUB_TOKEN'):
data = scan_contribs(Github(token), targets, datemode, datevar)
generate_stackbar(data, targets, datemode, datevar)
else:
print('GitHub auth token not found.')
if __name__ == '__main__':
main()