Rev 11: bring in hydrazine trunk in http://bzr.arbash-meinel.com/branches/bzr/other/hydrazine
John Arbash Meinel
john at arbash-meinel.com
Tue Feb 16 23:16:55 GMT 2010
At http://bzr.arbash-meinel.com/branches/bzr/other/hydrazine
------------------------------------------------------------
revno: 11 [merge]
revision-id: john at arbash-meinel.com-20100216231631-y2xf2jrmusdsdxez
parent: john at arbash-meinel.com-20100113211555-4b30wa7ro9n2v1xv
parent: mbp at sourcefrog.net-20100215232731-41u39nwr1a63os7b
committer: John Arbash Meinel <john at arbash-meinel.com>
branch nick: hydrazine
timestamp: Tue 2010-02-16 17:16:31 -0600
message:
bring in hydrazine trunk
added:
README readme-20100215011845-8kp9d05qw0jjc6fe-1
TODO todo-20100214214753-c2zbh895tcwirzp1-1
bugclient bugclient-20100214214833-o06q3ctm8nafs8eo-1
graph.sh graph.sh-20100205160544-j6q9emwwf3eya8zl-1
modified:
.bzrignore bzrignore-20091123060353-edo1gv4wsgwcwysu-1
capture-bug-counts.py capturebugcounts.py-20090921063150-4dxgpa869dp06zig-1
-------------- next part --------------
=== modified file '.bzrignore'
--- a/.bzrignore 2009-11-23 06:04:49 +0000
+++ b/.bzrignore 2010-02-14 23:12:51 +0000
@@ -1,1 +1,2 @@
.*.swp
+tmp.diff
=== added file 'README'
--- a/README 1970-01-01 00:00:00 +0000
+++ b/README 2010-02-15 01:18:53 +0000
@@ -0,0 +1,11 @@
+Hydrazine has some command-line Launchpad API clients.
+
+The most useful is bugclient::
+
+ ./bugclient
+ pillar bzr
+ select_new
+ bug 1234
+ status confirmed
+ importance high
+ tags +dirstate
=== added file 'TODO'
--- a/TODO 1970-01-01 00:00:00 +0000
+++ b/TODO 2010-02-15 01:00:35 +0000
@@ -0,0 +1,18 @@
+bugclient:
+
+perhaps keep some concept of the current context: a bug, a pillar, etc. then
+future operations are on that by default.
+
+run description etc through pager
+
+open in web browser
+
+go through bugs matching a query, eg incomplete or new in a particular product
+
+cleaner way to redo oauth authentication if you want to grant more access, or if the current token is expired
+
+save bugs lazily, only when quitting or moving on to the next one
+
+set tags, either incrementally or entirely
+
+show dates on bug
=== added file 'bugclient'
--- a/bugclient 1970-01-01 00:00:00 +0000
+++ b/bugclient 2010-02-15 23:27:31 +0000
@@ -0,0 +1,412 @@
+#! /usr/bin/python
+
+# Copyright (C) 2010 Martin Pool
+
+"""A text-mode interactive Launchpad client"""
+
+
+import cmd
+import httplib2
+import optparse
+import os
+import subprocess
+import sys
+
+import hydrazine
+import launchpadlib.launchpad
+
+
+class HydrazineCmd(cmd.Cmd):
+
+ def __init__(self):
+ cmd.Cmd.__init__(self)
+ self.bug = None
+ self.pillar = None
+ self.task_list = None
+
+ def _connect(self):
+ self.session = hydrazine.create_session()
+
+ def do_bug(self, bug_number):
+ """Open bug by number"""
+ try:
+ bug_number = int(bug_number)
+ except ValueError:
+ print 'usage: bzr NUMBER'
+ return
+ try:
+ the_bug = self.session.bugs[bug_number]
+ except KeyError:
+ print 'no such bug?'
+ return
+ self._select_bug(the_bug)
+
+ def do_comment(self, line):
+ """Post a comment to the current bug."""
+ if self._needs_bug(): return
+ if not line:
+ print "Please specify a comment"
+ return
+ result = self.bug.newMessage(content=line)
+ print "Posted message: %s" % result
+
+ def do_description(self, nothing):
+ """Show bug description"""
+ if self._needs_bug():
+ return
+ print self.bug.description
+
+ def do_duplicate(self, duplicate_id):
+ """Mark as a duplicate"""
+ if self._needs_bug():
+ return
+ try:
+ duplicate_id = int(duplicate_id)
+ except ValueError:
+ print 'usage: duplicate BUG_NUMBER'
+ return
+ # XXX: could just synthesize a URL, which might be faster; probably
+ # need to make sure the root lines up correctly
+ try:
+ duplicate_bug = self.session.bugs[duplicate_id]
+ except KeyError:
+ print 'no such bug?'
+ return
+ print 'marking %d as a duplicate of %d' % (self.current_bug_number,
+ duplicate_bug.id)
+ print ' "%s"' % duplicate_bug.title
+ self.bug.markAsDuplicate(duplicate_of=duplicate_bug)
+
+ def do_EOF(self, what):
+ return True
+
+ def do_importance(self, line):
+ """Set importance"""
+ task = self._needs_single_task()
+ if task is None:
+ return
+ new_importance = canonical_importance(line)
+ if new_importance is None:
+ return
+ print 'changing importance %s => %s' % (task.importance, new_importance)
+ task.importance = new_importance
+ task.lp_save()
+
+ def do_next(self, ignored):
+ """Go to the next bug in the list"""
+ if self.task_list is None:
+ print 'no list loaded; use select_new etc'
+ return
+ self.search_index += 1
+ bug_task = self.task_list[self.search_index]
+ self._select_bug(bug_task.bug)
+
+ def do_official_tags(self, ignored):
+ """Show the official tags for the current pillar."""
+ if self._needs_pillar(): return
+ print 'Official bug tags for %s' % self.pillar.name
+ tags = self.pillar.official_bug_tags
+ _show_columnated(tags)
+
+ def do_open(self, ignored):
+ """Open the current bug in a web browser"""
+ if self._needs_bug():
+ return
+ subprocess.call(['x-www-browser',
+ 'https://launchpad.net/bugs/%d' % self.current_bug_number])
+
+ def do_pillar(self, pillar_name):
+ """Select a pillar (project, etc)"""
+ self._select_pillar(self._find_pillar(pillar_name))
+
+ def do_refresh(self, ignored):
+ """Reload current bug."""
+ if self._needs_bug(): return
+ self.bug.lp_refresh()
+ self._show_bug(self.bug)
+
+ def do_retarget(self, to_pillar):
+ """Change a bug from this pillar to another."""
+ task = self._needs_single_task()
+ if task is None: return
+ if not to_pillar:
+ print 'usage: retarget TO_PILLAR'
+ return
+ new_target = self._find_pillar(to_pillar)
+ if new_target is None:
+ print 'no such product?'
+ return
+ print 'change target of bug %s' % (task.bug.id,)
+ print ' from: %s' % (task.target,)
+ print ' to: %s' % (new_target,)
+ task.target = new_target
+ task.lp_save()
+
+ def do_select_new(self, ignored):
+ """Select the list of new bugs in the current pillar"""
+ if self._needs_pillar(): return
+ self.task_list = self.pillar.searchTasks(status="New",
+ order_by=['-datecreated'])
+ self.task_list_index = 0
+ try:
+ first_bug_task = self.task_list[0]
+ self.search_index = 0
+ except IndexError:
+ print "No bugtasks found"
+ self._select_bug(first_bug_task.bug)
+
+ def do_show(self, ignored):
+ """Show the header of the current bug"""
+ if self._needs_bug():
+ return
+ self._show_bug(self.bug)
+
+ def do_title(self, new_title):
+ """Change the title of the current bug.
+
+example:
+ title bzr diff should warn if tree is out of date with branch
+ """
+ if self._needs_bug():
+ return
+ print 'changing title of bug %d to "%s"' % (self.bug.id, new_title)
+ print ' old title "%s"' % (self.bug.title)
+ self.bug.title = new_title
+ self.bug.lp_save()
+
+ def do_quit(self, ignored):
+ return True
+
+ def do_status(self, line):
+ """Change status of the current bug"""
+ task = self._needs_single_task()
+ if task is None:
+ return
+ new_status = canonical_status(line)
+ if new_status is None:
+ return
+ print 'changing status %s => %s' % (task.status, new_status)
+ task.status = new_status
+ task.lp_save()
+
+ def do_tags(self, line):
+ """Show, add or remove bug tags.
+
+example:
+ tags +easy -crash
+
+If no arguments are given, show the current tags.
+
+Otherwise, add or remove the given tags.
+"""
+ if self._needs_bug(): return
+ if not line.strip():
+ print 'bug %d tags: %s' % (self.bug.id, ' '.join(self.bug.tags))
+ return
+ to_add = []
+ to_remove = []
+ for word in line.split():
+ if word[0] == '+':
+ to_add.append(word[1:])
+ elif word[0] == '-':
+ to_remove.append(word[1:])
+ else:
+ # XXX: not sure, should we just set it?
+ to_add.append(word)
+ old_tags = list(self.bug.tags)
+ new_tags = old_tags[:]
+ for a in to_add:
+ if a not in new_tags:
+ new_tags.append(a)
+ for a in to_remove:
+ if a in new_tags:
+ new_tags.remove(a)
+ print 'changing bug %d tags' % self.bug.id
+ print ' from: %s' % ' '.join(old_tags)
+ print ' to: %s' % ' '.join(new_tags)
+ self.bug.tags = new_tags
+ self.bug.lp_save()
+
+ def do_triage(self, line):
+ """Set tags, status, and importance.
+
+example:
+ triage confirmed wishlist +foo +bar
+ """
+ if self._needs_bug(): return
+ task = self._needs_single_task()
+ if not task:
+ print 'no task selected'
+ return
+ for w in line.split():
+ if w[0] == '+':
+ self.bug.tags.append(w[1:])
+ continue
+ importance = canonical_importance(w)
+ if importance:
+ task.importance = importance
+ continue
+ status = canonical_status(w)
+ if status:
+ task.status = status
+ continue
+ if self.bug._dirty_attributes:
+ self.bug.lp_save()
+ if task._dirty_attributes:
+ task.lp_save()
+
+ def _needs_bug(self):
+ if self.bug is None:
+ print 'no bug selected'
+ return True
+
+ def _needs_pillar(self):
+ if self.pillar is None:
+ print 'no pillar selected'
+ return True
+
+ def _needs_single_task(self):
+ """Return the single task for the current bug in the current pillar, or None"""
+ if self.bug is None:
+ print 'no bug selected'
+ return None
+ tasks = list(self.bug.bug_tasks)
+ if self.pillar is None:
+ if len(tasks) == 1:
+ # no pillar; assume this is ok
+ return tasks[0]
+ else:
+ print 'This bug has multiple tasks; please choose a pillar'
+ return None
+ else:
+ for t in tasks:
+ if t.target == self.pillar:
+ return t
+ else:
+ print 'No task for %s in %s' % (self.pillar, self.bug)
+ return None
+
+ @property
+ def prompt(self):
+ p = 'hydrazine(%s) ' % (self.short_service_root,)
+ if self.bug is not None:
+ p += '#%d ' % (self.current_bug_number,)
+ if self.pillar is not None:
+ p += 'in %s ' % self.pillar.name
+ # would like to highlight the prompt, but Cmd doesn't seem to have a
+ # way to know some characters are not visible, therefore repainting is
+ # messed
+ if p[-1] == ' ':
+ p = p[:-1]
+ return p + '> '
+
+ def _select_bug(self, the_bug):
+ self.bug = the_bug
+ self.current_bug_number = the_bug.id
+ self._show_bug(self.bug)
+
+ def _find_pillar(self, pillar_name):
+ pillar_collection = self.session.pillars.search(text=pillar_name)
+ try:
+ return pillar_collection[0]
+ except IndexError:
+ print "No such pillar?"
+ return
+
+ def _select_pillar(self, pillar):
+ self.pillar = pillar
+ if pillar is None:
+ print "no pillar selected"
+ else:
+ print " %s" % self.pillar
+
+ def _show_bug(self, bug):
+ print 'bug: %d: %s' % (bug.id, bug.title)
+ if bug.duplicate_of:
+ print ' duplicate of bug %d' % (bug.duplicate_of.id,)
+ else:
+ for task in bug.bug_tasks:
+ print ' affects %-40s %14s %s' % (
+ task.bug_target_name, task.status, task.importance,)
+ print ' tags: %s' % ' '.join(bug.tags)
+
+
+def canonical_importance(from_importance):
+ real_importances = ['Critical', 'High', 'Medium', 'Low', 'Wishlist', 'Undecided']
+ return canonical_enum(from_importance, real_importances)
+
+
+def canonical_status(entered):
+ return canonical_enum(entered,
+ ['Confirmed', 'Triaged', 'Fix Committed', 'Fix Released', 'In Progress',
+ "Won't Fix", "Incomplete", "Invalid", "New"])
+
+
+def canonical_enum(entered, options):
+ def squish(a):
+ return a.lower().replace(' ', '')
+ for i in options:
+ if squish(i) == squish(entered):
+ return i
+ return None
+
+
+def _show_columnated(tags):
+ tags = tags[:]
+ longest = max(map(len, tags))
+ cols = int(os.environ.get('COLUMNS', '80'))
+ per_row = max(int((cols-1)/(longest + 1)), 1)
+ i = 0
+ while tags:
+ t = tags.pop(0)
+ print '%-*s' % (longest, t),
+ i += 1
+ if i == per_row:
+ print
+ i = 0
+ if i != 0:
+ print
+
+
+def main(argv):
+ parser = optparse.OptionParser()
+ parser.add_option('--staging', action='store_const',
+ const='staging',
+ dest='short_service_root')
+ parser.add_option('--debug', action='store_true',
+ dest='debug',
+ help='Show trace of API calls')
+ parser.add_option('-c', '--command',
+ action='append',
+ dest='commands',
+ help='Run this command before starting interactive mode (may be repeated)',
+ metavar='COMMAND',
+ )
+ parser.set_defaults(short_service_root='edge')
+
+ opts, args = parser.parse_args(argv)
+ hydrazine.service_root = dict(
+ edge=launchpadlib.launchpad.EDGE_SERVICE_ROOT,
+ staging=launchpadlib.launchpad.STAGING_SERVICE_ROOT,
+ )[opts.short_service_root]
+ if opts.debug:
+ # debuglevel only takes effect when the connection is opened, so we can't
+ # trivially change it while the program is running
+ # see <https://bugs.edge.launchpad.net/launchpadlib/+bug/520219>
+ httplib2.debuglevel = int(not httplib2.debuglevel)
+
+ cmd = HydrazineCmd()
+ cmd.short_service_root = opts.short_service_root
+ cmd._connect()
+
+ for c in opts.commands or []:
+ print '> ' + c
+ if cmd.onecmd(c):
+ break
+ else:
+ # run cmdloop unless eg '-c quit' caused us to exit already
+ cmd.cmdloop()
+
+
+if __name__ == '__main__':
+ main(sys.argv)
=== modified file 'capture-bug-counts.py'
--- a/capture-bug-counts.py 2009-11-23 06:15:53 +0000
+++ b/capture-bug-counts.py 2010-02-15 21:55:15 +0000
@@ -23,30 +23,46 @@
def run_external(args):
sys.stderr.write(">> %s\n" % args)
- rc = subprocess.call(args)= if rc != 0:
+ rc = subprocess.call(args)
+ if rc != 0:
sys.stderr.write("failed %s!" % rc)
raise AssertionError()
-## def count_bugs(bug_count, count_type, category):
-## rrd_file = os.path.join(
-## rrd_dir,
-## 'bugs_%s_%s.rrd' % (project_name, category.replace(' ', '')))
-## import pdb;pdb.set_trace()
-## if not os.path.exists(rrd_file):
-## # hourly data, aggregated to daily, kept up to 10 years
-## run_external([
-## 'rrdtool', 'create', rrd_file,
-## '--step', '3600',
-## 'DS:count:GAUGE:604800:0:U',
-## 'RRA:LAST:0.99:24:3650'])
-##
-## run_external([
-## 'rrdtool', 'update', rrd_file,
-## 'N@%d' % (bug_count),
-## ])
-##
-
+def trace(s):
+ sys.stderr.write(s + '\n')
+
+
+lplib_cachedir = os.path.expanduser("~/.cache/launchpadlib/")
+hydrazine_cachedir = os.path.expanduser("~/.cache/hydrazine/")
+rrd_dir = os.path.expanduser("~/.cache/hydrazine/rrd")
+for d in [lplib_cachedir, hydrazine_cachedir, rrd_dir]:
+ if not os.path.isdir(d):
+ os.makedirs(d, mode=0700)
+
+
+def create_session():
+ hydrazine_credentials_filename = os.path.join(hydrazine_cachedir,
+ 'credentials')
+ if os.path.exists(hydrazine_credentials_filename):
+ credentials = Credentials()
+ credentials.load(file(
+ os.path.expanduser("~/.cache/hydrazine/credentials"),
+ "r"))
+ trace('loaded existing credentials')
+ return Launchpad(credentials, service_root,
+ lplib_cachedir)
+ # TODO: handle the case of having credentials that have expired etc
+ else:
+ launchpad = Launchpad.get_token_and_login(
+ 'Hydrazine',
+ service_root,
+ lplib_cachedir)
+ trace('saving credentials...')
+ launchpad.credentials.save(file(
+ hydrazine_credentials_filename,
+ "w"))
+ return launchpad
def get_project():
sys.stderr.write('getting project... ')
@@ -64,7 +80,7 @@
def add(self, bt):
self.bugs.add(bt)
- def count(self):
+ def count_bugs(self):
return len(self.bugs)
def get_link_url(self):
@@ -96,43 +112,62 @@
% (project_name, self.status)
-def iter_to_list(it):
- # launchpad sometimes returns collections which refuse to be coerced to
- # list, but that can be forced
- l = []
- for a in it:
- l.append(a)
- return l
-
-
-def show_bug_status_table(project):
- has_patches = HasPatchBugCategory()
- sys.stderr.write('bugs with patches')
- for bt in project.searchTasks(has_patch=True):
- has_patches.add(bt)
- sys.stderr.write('.')
- sys.stderr.write('\n')
-
- sys.stderr.write('bugs by status')
- by_status = {}
- for bt in project.searchTasks()[:50]:
- if bt.status not in by_status:
- by_status[bt.status] = StatusBugCategory(bt.status)
- by_status[bt.status].add(bt)
- sys.stderr.write('.')
- sys.stderr.write('\n')
-
-
- all_categories = by_status.values() + [has_patches]
-
- for collection in all_categories:
- print '%6d %s %s' % (collection.count(),
- collection.get_name(),
- collection.get_link_url() or '')
-
-
-launchpad = create_session()
+class CannedQuery(object):
+
+ def __init__(self, project):
+ self.project = project
+
+ def _run_query(self, from_collection):
+ sys.stderr.write(self.get_name())
+ for bt in from_collection:
+ yield bt
+ sys.stderr.write('.')
+ sys.stderr.write('\n')
+
+ def show_text(self):
+ # print self.get_name()
+ for category in self.query_categories():
+ print '%6d %s %s' % (category.count_bugs(),
+ category.get_name(),
+ category.get_link_url() or '')
+ print
+
+
+class PatchCannedQuery(CannedQuery):
+
+ def get_collection(self):
+ return self.project.searchTasks(has_patch=True)
+
+ def get_name(self):
+ return 'Bugs with patches'
+
+ def query_categories(self):
+ has_patches = HasPatchBugCategory()
+ for bt in self._run_query(
+ self.project.searchTasks(has_patch=True)):
+ has_patches.add(bt)
+ return [has_patches]
+
+
+class StatusCannedQuery(CannedQuery):
+
+ def get_name(self):
+ return 'By Status'
+
+ def query_categories(self):
+ by_status = {}
+ for bugtask in self._run_query(self.project.searchTasks()):
+ if bugtask.status not in by_status:
+ by_status[bugtask.status] = StatusBugCategory(bugtask.status)
+ by_status[bugtask.status].add(bugtask)
+ return by_status.values()
+
+
+def show_bug_report(project):
+ for query_class in StatusCannedQuery, PatchCannedQuery:
+ query_class(project).show_text()
+
+
+launchpad = hydrazine.create_session()
project = get_project()
-show_bug_status_table(project)
-
-
+show_bug_report(project)
=== added file 'graph.sh'
--- a/graph.sh 1970-01-01 00:00:00 +0000
+++ b/graph.sh 2010-02-05 16:05:46 +0000
@@ -0,0 +1,14 @@
+#! /bin/sh -x
+
+cd ~/.cache/hydrazine/rrd
+product=bzr
+
+rrdtool graph bugs.png \
+DEF:Confirmed=bugs_${product}_Confirmed.rrd:count:LAST \
+DEF:InProgress=bugs_${product}_InProgress.rrd:count:LAST \
+DEF:FixCommitted=bugs_${product}_FixCommitted.rrd:count:LAST \
+DEF:Triaged=bugs_${product}_Triaged.rrd:count:LAST \
+DEF:Incomplete=bugs_${product}_Incomplete.rrd:count:LAST \
+LINE1:Confirmed\#aa0000:Confirmed \
+LINE1:InProgress\#888888:InProgress \
+LINE1:FixCommitted\#aa8800:FixCommitted
More information about the bazaar-commits
mailing list