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