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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
|
#!/usr/bin/python
#
# laika prints a summary of the Launchpad bugs you touched this week
#
# man's best friend guides you through Launchpad
# 'Laika died within hours after launch' => an aptly named tool ;)
# http://en.wikipedia.org/wiki/Laika
#
# Copyright 2010 Alex Chiang <achiang@canonical.com>
#
# This program is distributed under the terms of the
# GNU General Public License version 3.
import ConfigParser
import datetime
from optparse import OptionParser
import os
import re
import sys
from string import Template
from launchpadlib.launchpad import Launchpad
UTCNOW = datetime.datetime.utcnow()
class LPWrap:
'''Simple wrapper Cache-Proxy for LP objects'''
def __init__(self, lpObj):
self.lpObj = lpObj
def __getattr__(self, attr):
result = getattr(self.lpObj, attr)
# Tricky. We look at the attr, and if it is another launchpadlib
# object, then we wrap *it* too. e.g., you asked for task.bug, where
# bug is a launchpadlib object contained within the task lplib object.
# Eventually, we'll just get a normal attribute that isn't an lplib
# object, which we cache with setattr and then later retrieve with
# the above getattr.
if result.__class__.__name__ == "Entry":
result = LPWrap(result)
setattr(self, attr, result)
return result
class Report(object):
'''An activity report for a specified Launchpad user.
In order of decreasing importance, search and display activity on
bugs assigned to me, then on bugs that I actually commented upon,
and finally bugs that I just opened, but did nothing further with.
Avoids duplication of activity. So if the status was changed on
a bug assigned to you and you also commented on it, only report the
status change.
'''
bugs = {}
def __init__(self, user, window, bugpattern, ppas):
cachedir = os.path.expanduser("~/.launchpadlib/cache")
self.launchpad = Launchpad.login_with('laika', 'production', cachedir)
self.user = self.launchpad.people[user]
self.window = window
self.since = UTCNOW - datetime.timedelta(window)
self.bugpattern = bugpattern
self.ppas = ppas
self.status = ["New",
"Incomplete",
"Invalid",
"Won't Fix",
"Confirmed",
"Triaged",
"In Progress",
"Fix Committed",
"Fix Released" ]
def print_header(self, header):
print "==", header, "=="
def in_window(self, date):
'''Timezones do not exist, all datetime objects have to be naive.
Time zone aware means broken.
http://www.enricozini.org/2009/debian/using-python-datetime/
'''
win = datetime.timedelta(self.window)
date = date.replace(tzinfo=None)
delta = UTCNOW - date
return delta <= win
def print_bugid(self, task):
'''Using this interface adds the bug to global bug list.'''
ago = ""
self.bugs[task.bug.id] = 1
delta = UTCNOW - task.bug.date_last_updated.replace(tzinfo=None)
if delta.days > 0:
ago = "%d day%s" % (delta.days, "s" if delta.days > 1 else "")
hours = delta.seconds / 3600
if hours > 0:
ago += ", " if ago else ""
ago += "%d hour%s" % (hours, "s" if hours > 1 else "")
minutes = (delta.seconds - (hours * 3600)) / 60
if minutes > 0:
ago += ", " if ago else ""
ago += "%d minute%s" % (minutes, "s" if minutes > 1 else "")
print task.title
t = Template(self.bugpattern)
print t.substitute(bugid=str(task.bug.id))
print "last updated", ago, "ago"
def print_assignments(self):
statuses = ['closed', 'fix_released', 'fix_committed',
'in_progress', 'triaged', 'confirmed', 'created']
tasks = self.user.searchTasks(assignee=self.user,
status=self.status,
modified_since=self.since)
tasks = [LPWrap(t) for t in tasks]
self.print_header("Assigned Bugs")
for t in tasks:
if self.bugs.has_key(t.bug.id):
continue
updates = []
for s in statuses:
attr = 'date_' + s
date = getattr(t, attr)
if not date:
continue
if self.in_window(date):
updates.append(
"\t" + s + ": " + re.sub("\s.*$", "", str(date)))
if updates:
self.print_bugid(t)
for u in updates:
print u
print
print
def print_comments(self):
tasks = self.user.searchTasks(bug_commenter=self.user,
status=self.status,
modified_since=self.since)
tasks = [LPWrap(t) for t in tasks]
self.print_header("Commented Bugs")
for t in tasks:
if self.bugs.has_key(t.bug.id):
continue
for m in t.bug.messages:
if m.owner_link != self.user.self_link:
continue
if self.in_window(m.date_created):
self.print_bugid(t)
print
break
print
def print_reported(self):
tasks = self.user.searchTasks(bug_reporter=self.user,
status=self.status,
modified_since=self.since)
tasks = [LPWrap(t) for t in tasks]
self.print_header("Reported Bugs")
for t in tasks:
if self.bugs.has_key(t.bug.id):
continue
if self.in_window(t.bug.date_created):
self.print_bugid(t)
print
def print_ppa_activity(self):
if self.ppas == '':
return
# Handle command line case, where user passes in comma-separated list
self.ppas = re.sub(",", "\n", self.ppas)
# Now each PPA is separated by a newline, which is how we might
# expect them if they're specified in the config file.
ppas = [p for p in self.ppas.split("\n") if p != '']
self.print_header("PPA Activity")
for ppa in ppas:
# A PPA seems to always belong to a person (or a team, which
# is a person in LP) so assuming "person/PPA" sounds reasonable.
person, archive = ppa.split('/')
owner = self.launchpad.people[person]
archive = owner.getPPAByName(name=archive)
sources = archive.getPublishedSources()
print ppa
for s in sources:
if s.package_signer != self.user:
continue
# There might be yet-unpublished packages in the PPA, which
# would then have NoneType for date_published. Don't check
# in_window in that case.
if s.date_published:
if self.in_window(s.date_published):
print "\t", s.display_name
print
print
def render(self):
self.print_assignments()
self.print_comments()
self.print_reported()
self.print_ppa_activity()
def get_config(opts):
config = {}
FORMAT = "1.0"
laika_config = ConfigParser.ConfigParser()
laika_config.read(os.path.expanduser("~/.laikarc"))
def get_bzrlplogin():
path = os.path.expanduser('~/.bazaar/bazaar.conf')
if not os.path.exists(path):
return None
else:
cfg = ConfigParser.ConfigParser()
cfg.read(os.path.expanduser('~/.bazaar/bazaar.conf'))
return cfg.get('DEFAULT', 'launchpad_username')
def default_user():
return get_bzrlplogin() or os.getenv("USER")
def default_window():
return 8
def default_bugpattern():
return 'https://launchpad.net/bugs/$bugid'
def default_ppas():
return ''
for opt in opts:
try:
config[opt] = laika_config.get(FORMAT, opt)
except:
config[opt] = locals()["default_%s" % opt]()
return config
def main():
opts = ['user', 'window', 'bugpattern', 'ppas']
config = get_config(opts)
parser = OptionParser()
parser.add_option('-u', '--user', dest='user',
default=config['user'],
help='Specify the Launchpad user id. '
'Defaults to session username.')
parser.add_option('-w', '--window', dest='window', type='int',
default=config['window'],
help='Number of days of past activity to look for. '
'Defaults to 8 days')
parser.add_option('-b', '--bug-pattern', dest='bugpattern', type='string',
default=config['bugpattern'],
help='Use this string when displaying a bug reference. '
'Occurrences of $bugid will be replaced by the actual bug number. '
'Defaults to \'https://launchpad.net/bugs/$bugid\'')
parser.add_option('--ppas', dest='ppas',
default=config['ppas'],
help='Search for activity in specified PPAs')
options, arguments = parser.parse_args()
report = Report(options.user, options.window, options.bugpattern,
options.ppas)
report.render()
if __name__ == "__main__":
main()
|