From 091e7386f730c17961e8c3b4d80fcf667d29d718 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Tue, 25 Mar 2008 17:30:30 -0400 Subject: Woo doggies a big chunk of abstractification. --- bugzilla/rhbugzilla.py | 189 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 bugzilla/rhbugzilla.py (limited to 'bugzilla/rhbugzilla.py') diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py new file mode 100644 index 0000000..5bfaabb --- /dev/null +++ b/bugzilla/rhbugzilla.py @@ -0,0 +1,189 @@ +# rhbugzilla.py - a Python interface to Red Hat Bugzilla using xmlrpclib. +# +# Copyright (C) 2008 Red Hat Inc. +# Author: Will Woods +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at your +# option) any later version. See http://www.gnu.org/copyleft/gpl.html for +# the full text of the license. + +import bugzilla.base + +version = '0.1' +user_agent = bugzilla.base.user_agent + ' RHBugzilla/%s' % version + +class RHBugzilla(bugzilla.base.BugzillaBase): + '''Concrete implementation of the Bugzilla protocol. This one uses the + methods provided by Red Hat's Bugzilla 2.18 variant.''' + def __init__(self,**kwargs): + bugzilla.base.BugzillaBase.__init__(self,**kwargs) + self.user_agent = user_agent + + #---- Methods and properties with basic bugzilla info + + # Connect the backend methods to the XMLRPC methods + def _getbugfields(self): + return self._proxy.bugzilla.getBugFields(self.user,self.password) + def _getqueryinfo(self): + return self._proxy.bugzilla.getQueryInfo(self.user,self.password) + def _getproducts(self): + return self._proxy.bugzilla.getProdInfo(self.user, self.password) + def _getcomponents(self,product): + return self._proxy.bugzilla.getProdCompInfo(product,self.user,self.password) + def _getcomponentsdetails(self,product): + return self._proxy.bugzilla.getProdCompDetails(product,self.user,self.password) + + #---- Methods for reading bugs and bug info + + def _getbug(self,id): + '''Return a dict of full bug info for the given bug id''' + return self._proxy.bugzilla.getBug(id, self.user, self.password) + def _getbugsimple(self,id): + '''Return a short dict of simple bug info for the given bug id''' + r = self._proxy.bugzilla.getBugSimple(id, self.user, self.password) + if r and 'bug_id' not in r: + # XXX hurr. getBugSimple doesn't fault if the bug is missing. + # Let's synthesize one ourselves. + raise xmlrpclib.Fault("Server","Could not load bug %s" % id) + else: + return r + def _query(self,query): + '''Query bugzilla and return a list of matching bugs. + query must be a dict with fields like those in in querydata['fields']. + Returns a dict like this: {'bugs':buglist, + 'displaycolumns':columnlist, + 'sql':querystring} + buglist is a list of dicts describing bugs. You can specify which + columns/keys will be listed in the bugs by setting 'column_list' in + the query; otherwise the default columns are used (see the list in + querydefaults['default_column_list']). The list of columns will be + in 'displaycolumns', and the SQL query used by this query will be in + 'sql'. + ''' + return self._proxy.bugzilla.runQuery(query,self.user,self.password) + + #---- Methods for modifying existing bugs. + + # Most of these will probably also be available as Bug methods, e.g.: + # Bugzilla.setstatus(id,status) -> + # Bug.setstatus(status): self.bugzilla.setstatus(self.bug_id,status) + + def _addcomment(self,id,comment,private=False, + timestamp='',worktime='',bz_gid=''): + '''Add a comment to the bug with the given ID. Other optional + arguments are as follows: + private: if True, mark this comment as private. + timestamp: comment timestamp, in the form "YYYY-MM-DD HH:MM:SS" + worktime: amount of time spent on this comment (undoc in upstream) + bz_gid: if present, and the entire bug is *not* already private + to this group ID, this comment will be marked private. + ''' + return self._proxy.bugzilla.addComment(id,comment, + self.user,self.password,private,timestamp,worktime,bz_gid) + def _setstatus(self,id,status,comment='',private=False,private_in_it=False,nomail=False): + '''Set the status of the bug with the given ID. You may optionally + include a comment to be added, and may further choose to mark that + comment as private. + The status may be anything from querydefaults['bug_status_list']. + Common statuses: 'NEW','ASSIGNED','MODIFIED','NEEDINFO' + Less common: 'VERIFIED','ON_DEV','ON_QA','REOPENED' + 'CLOSED' is not valid with this method; use closebug() instead. + ''' + return self._proxy.bugzilla.changeStatus(id,status, + self.user,self.password,comment,private,private_in_it,nomail) + def _closebug(self,id,resolution,dupeid,fixedin,comment,isprivate,private_in_it,nomail): + '''Raw xmlrpc call for closing bugs. Documentation from Bug.pm is + below. Note that we drop the username and password fields because the + Bugzilla object contains them already. + + closeBug($bugid, $new_resolution, $username, $password, $dupeid, + $new_fixed_in, $comment, $isprivate, $private_in_it, $nomail) + + Close a current Bugzilla bug report with a specific resolution. This will eventually be done in Bugzilla/Bug.pm + instead and is meant to only be a quick fix. Please use bugzilla.changesStatus to changed to an opened state. + This method will change the bug report's status to CLOSED. + + $bugid + # ID of bug report to add comment to. + $new_resolution + # Valid Bugzilla resolution to transition the report into. + # DUPLICATE requires $dupeid to be passed in. + $dupeid + # Bugzilla report ID that this bug is being closed as + # duplicate of. + # Requires $new_resolution to be DUPLICATE. + $new_fixed_in + # OPTIONAL String representing version of product/component + # that bug is fixed in. + $comment + # OPTIONAL Text string containing comment to add. + $isprivate + # OPTIONAL Whether the comment will be private to the + # 'private_comment' Bugzilla group. + # Default: false + $private_in_it + # OPTIONAL if true will make the comment private in + # Issue Tracker + # Default: follows $isprivate + $nomail + # OPTIONAL Flag that is either 1 or 0 if you want email to be sent or not for this change + ''' + return self._proxy.bugzilla.closeBug(id,resolution,self.user,self.password, + dupeid,fixedin,comment,isprivate,private_in_it,nomail) + def _setassignee(self,id,**data): + '''Raw xmlrpc call to set one of the assignee fields on a bug. + changeAssignment($id, $data, $username, $password) + data: 'assigned_to','reporter','qa_contact','comment' + returns: [$id, $mailresults]''' + return self._proxy.bugzilla.changeAssignment(id,data,self.user,self.password) + def _updatedeps(self,id,deplist): + '''IMPLEMENT ME: update the deps (blocked/dependson) for the given bug. + updateDepends($bug_id,$data,$username,$password,$nodependencyemail) + #data: 'blocked'=>id,'dependson'=>id,'action' => ('add','remove')''' + raise NotImplementedError + def _updatecc(self,id,cclist,action,comment='',nomail=False): + '''Updates the CC list using the action and account list specified. + cclist must be a list (not a tuple!) of addresses. + action may be 'add', 'remove', or 'makeexact'. + comment specifies an optional comment to add to the bug. + if mail is True, email will be generated for this change. + ''' + data = {'id':id, 'action':action, 'cc':','.join(cclist), + 'comment':comment, 'nomail':nomail} + return self._proxy.bugzilla.updateCC(data,self.user,self.password) + def _updatewhiteboard(self,id,text,which,action): + '''Update the whiteboard given by 'which' for the given bug. + performs the given action (which may be 'append',' prepend', or + 'overwrite') using the given text.''' + data = {'type':which,'text':text,'action':action} + return self._proxy.bugzilla.updateWhiteboard(id,data,self.user,self.password) + # TODO: update this when the XMLRPC interface grows requestee support + def _updateflags(self,id,flags): + '''Updates the flags associated with a bug report. + data should be a hash of {'flagname':'value'} pairs, like so: + {'needinfo':'?','fedora-cvs':'+'} + You may also add a "nomail":1 item, which will suppress email if set. + + NOTE: the Red Hat XMLRPC interface does not yet support setting the + requestee (as in: needinfo from smartguy@answers.com). Alas.''' + return self._proxy.bugzilla.updateFlags(id,flags,self.user,self.password) + + #---- Methods for working with attachments + + # If your bugzilla wants attachments in something other than base64, you + # should override _attachment_encode here. + # If your bugzilla uses non-standard paths for attachment.cgi, you'll + # want to override _attachment_uri here. + + def _attachfile(self,id,**attachdata): + return self._proxy.bugzilla.addAttachment(id,attachdata,self.user,self.password) + + #---- createbug - call to create a new bug + + def _createbug(self,**data): + '''Raw xmlrpc call for createBug() Doesn't bother guessing defaults + or checking argument validity. Use with care. + Returns [bug_id, mailresults]''' + return self._proxy.bugzilla.createBug(data,self.user,self.password) -- cgit From dbd928a4b7ec66e10cf4b3c0e310ca211d76bbc2 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Fri, 6 Jun 2008 09:38:52 -0400 Subject: Fix cookie handling for bugzilla installations not on root of host, add some logging, abstractify login method --- bugzilla/rhbugzilla.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'bugzilla/rhbugzilla.py') diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index 5bfaabb..0c905ac 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -21,6 +21,10 @@ class RHBugzilla(bugzilla.base.BugzillaBase): bugzilla.base.BugzillaBase.__init__(self,**kwargs) self.user_agent = user_agent + def _login(self,user,password): + '''Backend login method for RHBugzilla.''' + return self._proxy.bugzilla.login(user,password) + #---- Methods and properties with basic bugzilla info # Connect the backend methods to the XMLRPC methods @@ -185,5 +189,6 @@ class RHBugzilla(bugzilla.base.BugzillaBase): def _createbug(self,**data): '''Raw xmlrpc call for createBug() Doesn't bother guessing defaults or checking argument validity. Use with care. - Returns [bug_id, mailresults]''' - return self._proxy.bugzilla.createBug(data,self.user,self.password) + Returns bug_id''' + r = self._proxy.bugzilla.createBug(data,self.user,self.password) + return r[0] -- cgit From 4782cbf51cdf89faa977ccace668f75e97e21e6d Mon Sep 17 00:00:00 2001 From: Will Woods Date: Tue, 10 Jun 2008 11:21:32 -0400 Subject: stop sending passwords with rhbugzilla requests - rely on the login() cookie everywhere --- bugzilla/rhbugzilla.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) (limited to 'bugzilla/rhbugzilla.py') diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index 0c905ac..c5964e2 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -11,7 +11,7 @@ import bugzilla.base -version = '0.1' +version = '0.2' user_agent = bugzilla.base.user_agent + ' RHBugzilla/%s' % version class RHBugzilla(bugzilla.base.BugzillaBase): @@ -29,24 +29,24 @@ class RHBugzilla(bugzilla.base.BugzillaBase): # Connect the backend methods to the XMLRPC methods def _getbugfields(self): - return self._proxy.bugzilla.getBugFields(self.user,self.password) + return self._proxy.bugzilla.getBugFields() def _getqueryinfo(self): - return self._proxy.bugzilla.getQueryInfo(self.user,self.password) + return self._proxy.bugzilla.getQueryInfo() def _getproducts(self): - return self._proxy.bugzilla.getProdInfo(self.user, self.password) + return self._proxy.bugzilla.getProdInfo() def _getcomponents(self,product): - return self._proxy.bugzilla.getProdCompInfo(product,self.user,self.password) + return self._proxy.bugzilla.getProdCompInfo(product) def _getcomponentsdetails(self,product): - return self._proxy.bugzilla.getProdCompDetails(product,self.user,self.password) + return self._proxy.bugzilla.getProdCompDetails(product) #---- Methods for reading bugs and bug info def _getbug(self,id): '''Return a dict of full bug info for the given bug id''' - return self._proxy.bugzilla.getBug(id, self.user, self.password) + return self._proxy.bugzilla.getBug(id) def _getbugsimple(self,id): '''Return a short dict of simple bug info for the given bug id''' - r = self._proxy.bugzilla.getBugSimple(id, self.user, self.password) + r = self._proxy.bugzilla.getBugSimple(id) if r and 'bug_id' not in r: # XXX hurr. getBugSimple doesn't fault if the bug is missing. # Let's synthesize one ourselves. @@ -66,7 +66,7 @@ class RHBugzilla(bugzilla.base.BugzillaBase): in 'displaycolumns', and the SQL query used by this query will be in 'sql'. ''' - return self._proxy.bugzilla.runQuery(query,self.user,self.password) + return self._proxy.bugzilla.runQuery(query) #---- Methods for modifying existing bugs. @@ -84,8 +84,8 @@ class RHBugzilla(bugzilla.base.BugzillaBase): bz_gid: if present, and the entire bug is *not* already private to this group ID, this comment will be marked private. ''' - return self._proxy.bugzilla.addComment(id,comment, - self.user,self.password,private,timestamp,worktime,bz_gid) + return self._proxy.bugzilla.addComment(id,comment,self.user,'', + private,timestamp,worktime,bz_gid) def _setstatus(self,id,status,comment='',private=False,private_in_it=False,nomail=False): '''Set the status of the bug with the given ID. You may optionally include a comment to be added, and may further choose to mark that @@ -95,8 +95,8 @@ class RHBugzilla(bugzilla.base.BugzillaBase): Less common: 'VERIFIED','ON_DEV','ON_QA','REOPENED' 'CLOSED' is not valid with this method; use closebug() instead. ''' - return self._proxy.bugzilla.changeStatus(id,status, - self.user,self.password,comment,private,private_in_it,nomail) + return self._proxy.bugzilla.changeStatus(id,status,self.user,'', + comment,private,private_in_it,nomail) def _closebug(self,id,resolution,dupeid,fixedin,comment,isprivate,private_in_it,nomail): '''Raw xmlrpc call for closing bugs. Documentation from Bug.pm is below. Note that we drop the username and password fields because the @@ -134,14 +134,14 @@ class RHBugzilla(bugzilla.base.BugzillaBase): $nomail # OPTIONAL Flag that is either 1 or 0 if you want email to be sent or not for this change ''' - return self._proxy.bugzilla.closeBug(id,resolution,self.user,self.password, + return self._proxy.bugzilla.closeBug(id,resolution,self.user,'', dupeid,fixedin,comment,isprivate,private_in_it,nomail) def _setassignee(self,id,**data): '''Raw xmlrpc call to set one of the assignee fields on a bug. changeAssignment($id, $data, $username, $password) data: 'assigned_to','reporter','qa_contact','comment' returns: [$id, $mailresults]''' - return self._proxy.bugzilla.changeAssignment(id,data,self.user,self.password) + return self._proxy.bugzilla.changeAssignment(id,data) def _updatedeps(self,id,deplist): '''IMPLEMENT ME: update the deps (blocked/dependson) for the given bug. updateDepends($bug_id,$data,$username,$password,$nodependencyemail) @@ -156,13 +156,13 @@ class RHBugzilla(bugzilla.base.BugzillaBase): ''' data = {'id':id, 'action':action, 'cc':','.join(cclist), 'comment':comment, 'nomail':nomail} - return self._proxy.bugzilla.updateCC(data,self.user,self.password) + return self._proxy.bugzilla.updateCC(data) def _updatewhiteboard(self,id,text,which,action): '''Update the whiteboard given by 'which' for the given bug. performs the given action (which may be 'append',' prepend', or 'overwrite') using the given text.''' data = {'type':which,'text':text,'action':action} - return self._proxy.bugzilla.updateWhiteboard(id,data,self.user,self.password) + return self._proxy.bugzilla.updateWhiteboard(id,data) # TODO: update this when the XMLRPC interface grows requestee support def _updateflags(self,id,flags): '''Updates the flags associated with a bug report. @@ -172,7 +172,7 @@ class RHBugzilla(bugzilla.base.BugzillaBase): NOTE: the Red Hat XMLRPC interface does not yet support setting the requestee (as in: needinfo from smartguy@answers.com). Alas.''' - return self._proxy.bugzilla.updateFlags(id,flags,self.user,self.password) + return self._proxy.bugzilla.updateFlags(id,flags) #---- Methods for working with attachments @@ -182,7 +182,7 @@ class RHBugzilla(bugzilla.base.BugzillaBase): # want to override _attachment_uri here. def _attachfile(self,id,**attachdata): - return self._proxy.bugzilla.addAttachment(id,attachdata,self.user,self.password) + return self._proxy.bugzilla.addAttachment(id,attachdata) #---- createbug - call to create a new bug @@ -190,5 +190,5 @@ class RHBugzilla(bugzilla.base.BugzillaBase): '''Raw xmlrpc call for createBug() Doesn't bother guessing defaults or checking argument validity. Use with care. Returns bug_id''' - r = self._proxy.bugzilla.createBug(data,self.user,self.password) + r = self._proxy.bugzilla.createBug(data) return r[0] -- cgit From 68299ba63acc14fc38f2dc2c7fd1373c0aff1119 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Tue, 10 Jun 2008 14:54:15 -0400 Subject: Explicitly mark methods unsupported by bugzilla3.0. Change the format of the .product attribute to be more like Bugzilla 3.x. --- bugzilla/rhbugzilla.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'bugzilla/rhbugzilla.py') diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index c5964e2..a069a2b 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -33,10 +33,24 @@ class RHBugzilla(bugzilla.base.BugzillaBase): def _getqueryinfo(self): return self._proxy.bugzilla.getQueryInfo() def _getproducts(self): - return self._proxy.bugzilla.getProdInfo() + '''Backend _getproducts method for RH Bugzilla. This predates the + Bugzilla3 Products stuff, so we need to massage this data to make it + fit the proper format''' + r = self._proxy.bugzilla.getProdInfo() + n = 0 + prod = [] + for name,desc in r.iteritems(): + # We're making up a fake id, since RHBugzilla doesn't use them + prod.append({'id':n,'name':name,'description':desc}) + n += 1 + return prod def _getcomponents(self,product): + if type(product) == int: + product = self._product_id_to_name(product) return self._proxy.bugzilla.getProdCompInfo(product) def _getcomponentsdetails(self,product): + if type(product) == int: + product = self._product_id_to_name(product) return self._proxy.bugzilla.getProdCompDetails(product) #---- Methods for reading bugs and bug info -- cgit From 02454166d136f69ec859660dcea040acaee850cd Mon Sep 17 00:00:00 2001 From: Will Woods Date: Tue, 5 Aug 2008 16:11:24 -0400 Subject: Add disconnect() and logout() methods --- bugzilla/rhbugzilla.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'bugzilla/rhbugzilla.py') diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index a069a2b..09b09a4 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -25,6 +25,13 @@ class RHBugzilla(bugzilla.base.BugzillaBase): '''Backend login method for RHBugzilla.''' return self._proxy.bugzilla.login(user,password) + def _logout(self): + '''Backend logout method for RHBugzilla.''' + # "Logouts are not implemented due to the non-session nature of + # XML-RPC communication." + # That's funny, since we get a (session-based) login cookie... + return True + #---- Methods and properties with basic bugzilla info # Connect the backend methods to the XMLRPC methods -- cgit From 5c54609e23fca1104e195ad4755b6ca098db6759 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Fri, 8 Aug 2008 09:59:14 -0400 Subject: Move multicall methods into rhbugzilla, since normal bugzilla instances don't support it --- bugzilla/rhbugzilla.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'bugzilla/rhbugzilla.py') diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index 09b09a4..d19c863 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -74,6 +74,28 @@ class RHBugzilla(bugzilla.base.BugzillaBase): raise xmlrpclib.Fault("Server","Could not load bug %s" % id) else: return r + # Multicall methods + def _getbugs(self,idlist): + '''Like _getbug, but takes a list of ids and returns a corresponding + list of bug objects. Uses multicall for awesome speed.''' + mc = self._multicall() + for id in idlist: + mc._getbug(id) + raw_results = mc.run() + del mc + # check results for xmlrpc errors, and replace them with None + return replace_getbug_errors_with_None(raw_results) + def _getbugssimple(self,idlist): + '''Like _getbugsimple, but takes a list of ids and returns a + corresponding list of bug objects. Uses multicall for awesome speed.''' + mc = self._multicall() + for id in idlist: + mc._getbugsimple(id) + raw_results = mc.run() + del mc + # check results for xmlrpc errors, and replace them with None + return replace_getbug_errors_with_None(raw_results) + def _query(self,query): '''Query bugzilla and return a list of matching bugs. query must be a dict with fields like those in in querydata['fields']. -- cgit From d4f2581e5cb6fa11b9510c2e0a3ea6e52411fec4 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Fri, 15 Aug 2008 12:43:27 -0400 Subject: Modify _updatedeps slightly (it wasn't being used by anything anyway) and implement it for bz32 and rhbz --- bugzilla/rhbugzilla.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) (limited to 'bugzilla/rhbugzilla.py') diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index d19c863..984787b 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -185,11 +185,29 @@ class RHBugzilla(bugzilla.base.BugzillaBase): data: 'assigned_to','reporter','qa_contact','comment' returns: [$id, $mailresults]''' return self._proxy.bugzilla.changeAssignment(id,data) - def _updatedeps(self,id,deplist): - '''IMPLEMENT ME: update the deps (blocked/dependson) for the given bug. + def _updatedeps(self,id,blocked,dependson,action): + '''update the deps (blocked/dependson) for the given bug. + blocked/dependson: list of bug ids/aliases + action: 'add' or 'delete' + + RHBZ call: updateDepends($bug_id,$data,$username,$password,$nodependencyemail) - #data: 'blocked'=>id,'dependson'=>id,'action' => ('add','remove')''' - raise NotImplementedError + #data: 'blocked'=>id,'dependson'=>id,'action' => ('add','remove') + + RHBZ only does one bug at a time, so this method will loop through + the blocked/dependson lists. This may be slow. + ''' + r = [] + if action == 'delete': + action == 'remove' + data = {'id':id, 'action':action, 'blocked':'', 'dependson':''} + for b in blocked: + data['blocked'] = b + self._proxy.bugzilla.updateDepends(id,data) + data['blocked'] = '' + for d in dependson: + data['dependson'] = d + self._proxy.bugzilla.updateDepends(id,data) def _updatecc(self,id,cclist,action,comment='',nomail=False): '''Updates the CC list using the action and account list specified. cclist must be a list (not a tuple!) of addresses. -- cgit From 772bb9f201521535df57fa8b37d2da2af1c3a5fb Mon Sep 17 00:00:00 2001 From: Will Woods Date: Mon, 18 Aug 2008 13:40:25 -0400 Subject: Fix up _updatecc - change the action param to match other methods and implement it for bz32. --- bugzilla/rhbugzilla.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'bugzilla/rhbugzilla.py') diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index 984787b..a0a5656 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -211,10 +211,15 @@ class RHBugzilla(bugzilla.base.BugzillaBase): def _updatecc(self,id,cclist,action,comment='',nomail=False): '''Updates the CC list using the action and account list specified. cclist must be a list (not a tuple!) of addresses. - action may be 'add', 'remove', or 'makeexact'. + action may be 'add', 'delete', or 'overwrite'. comment specifies an optional comment to add to the bug. if mail is True, email will be generated for this change. ''' + # Massage the 'action' param into what the old updateCC call expects + if action == 'delete': + action = 'remove' + elif action == 'overwrite': + action = 'makeexact' data = {'id':id, 'action':action, 'cc':','.join(cclist), 'comment':comment, 'nomail':nomail} return self._proxy.bugzilla.updateCC(data) -- cgit From 037f5df1207c86d6a7ca0aeff6f584d66aee7d75 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Mon, 18 Aug 2008 18:00:11 -0400 Subject: comment cleanups --- bugzilla/rhbugzilla.py | 1 + 1 file changed, 1 insertion(+) (limited to 'bugzilla/rhbugzilla.py') diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index a0a5656..6ea8902 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -198,6 +198,7 @@ class RHBugzilla(bugzilla.base.BugzillaBase): the blocked/dependson lists. This may be slow. ''' r = [] + # Massage input to match what RHBZ expects if action == 'delete': action == 'remove' data = {'id':id, 'action':action, 'blocked':'', 'dependson':''} -- cgit From 2cbd34ec163f503648245f2076f5601ed9498de6 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Tue, 19 Aug 2008 14:06:18 -0400 Subject: Move bugzilla3.RHBugzilla32 to rhbugzilla.RHBugzilla3, and import that in toplevel bugzilla module --- bugzilla/rhbugzilla.py | 150 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) (limited to 'bugzilla/rhbugzilla.py') diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index 6ea8902..647d8bc 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -10,6 +10,7 @@ # the full text of the license. import bugzilla.base +from bugzilla3 import Bugzilla32 version = '0.2' user_agent = bugzilla.base.user_agent + ' RHBugzilla/%s' % version @@ -259,3 +260,152 @@ class RHBugzilla(bugzilla.base.BugzillaBase): Returns bug_id''' r = self._proxy.bugzilla.createBug(data) return r[0] + +class RHBugzilla3(Bugzilla32, RHBugzilla): + '''Concrete implementation of the Bugzilla protocol. This one uses the + methods provided by Red Hat's Bugzilla 3.1.4+ instance, which is a superset + of the Bugzilla 3.2 methods. The additional methods (Bug.search, Bug.update) + should make their way into a later upstream Bugzilla release (probably 4.0). + + Note that RHBZ3 *also* supports most of the old RHBZ methods, under the + 'bugzilla' namespace. + + This class was written using bugzilla.redhat.com's API docs: + https://bugzilla.redhat.com/docs/en/html/api/ + ''' + + version = '0.1' + user_agent = bugzilla.base.user_agent + ' RHBugzilla3/%s' % version + + def _query(self,query): + '''Query bugzilla and return a list of matching bugs. + query must be a dict with fields like those in in querydata['fields']. + You can also pass in keys called 'quicksearch' or 'savedsearch' - + 'quicksearch' will do a quick keyword search like the simple search + on the Bugzilla home page. + 'savedsearch' should be the name of a previously-saved search to + execute. You need to be logged in for this to work. + Returns a dict like this: {'bugs':buglist, + 'sql':querystring} + buglist is a list of dicts describing bugs, and 'sql' contains the SQL + generated by executing the search. + ''' + return self._proxy.Bug.search(query) + + #---- Methods for updating bugs. + + def _update_bugs(self,ids,updates): + '''Update the given fields with the given data in one or more bugs. + ids should be a list of integers or strings, representing bug ids or + aliases. + updates is a dict containing pairs like so: {'fieldname':'newvalue'} + ''' + # TODO document changeable fields & return values + # TODO I think we need to catch XMLRPC exceptions to get a useful + # return value + return self._proxy.Bug.update({'ids':ids,'updates':updates}) + + def _update_bug(self,id,updates): + '''Update a single bug, specified by integer ID or (string) bug alias. + Really just a convenience method for _update_bugs(ids=[id],updates)''' + return self._update_bugs(ids=[id],updates=updates) + + # Eventually - when RHBugzilla is well and truly obsolete - we'll delete + # all of these methods and refactor the Base Bugzilla object so all the bug + # modification calls go through _update_bug. + # Until then, all of these methods are basically just wrappers around it. + + # TODO: allow multiple bug IDs + + def _setstatus(self,id,status,comment='',private=False,private_in_it=False,nomail=False): + '''Set the status of the bug with the given ID.''' + update={'bug_status':status} + if comment: + update['comment'] = comment + return self._update_bug(id,update) + + def _closebug(self,id,resolution,dupeid,fixedin,comment,isprivate,private_in_it,nomail): + '''Close the given bug. This is the raw call, and no data checking is + done here. That's up to the closebug method. + Note that the private_in_it and nomail args are ignored.''' + update={'bug_status':'CLOSED','resolution':resolution} + if dupeid: + update['resolution'] = 'DUPLICATE' + update['dupe_id'] = dupeid + if fixedin: + update['fixed_in'] = fixedin + if comment: + update['comment'] = comment + if isprivate: + update['commentprivacy'] = True + return self._update_bug(id,update) + + def _setassignee(self,id,**data): + '''Raw xmlrpc call to set one of the assignee fields on a bug. + changeAssignment($id, $data, $username, $password) + data: 'assigned_to','reporter','qa_contact','comment' + returns: [$id, $mailresults]''' + # drop empty items + update = dict([(k,v) for k,v in data.iteritems() if v != '']) + return self._update_bug(id,update) + + def _updatedeps(self,id,blocked,dependson,action): + '''Update the deps (blocked/dependson) for the given bug. + blocked, dependson: list of bug ids/aliases + action: 'add' or 'delete' + ''' + if action not in ('add','delete'): + raise ValueError, "action must be 'add' or 'delete'" + update={'%s_blocked' % action: blocked, + '%s_dependson' % action: dependson} + self._update_bug(id,update) + + def _updatecc(self,id,cclist,action,comment='',nomail=False): + '''Updates the CC list using the action and account list specified. + cclist must be a list (not a tuple!) of addresses. + action may be 'add', 'delete', or 'overwrite'. + comment specifies an optional comment to add to the bug. + if mail is True, email will be generated for this change. + ''' + update = {} + if comment: + update['comment'] = comment + + if action in ('add','delete'): + update['%s_cc' % action] = cclist + self._update_bug(id,update) + elif action == 'overwrite': + r = self._getbug(id) + if 'cc' not in r: + raise AttributeError, "Can't find cc list in bug %s" % str(id) + self._updatecc(id,r['cc'],'delete') + self._updatecc(id,cclist,'add') + # FIXME we don't check inputs on other backend methods, maybe this + # is more appropriate in the public method(s) + else: + raise ValueError, "action must be 'add','delete', or 'overwrite'" + + def _updatewhiteboard(self,id,text,which,action): + '''Update the whiteboard given by 'which' for the given bug. + performs the given action (which may be 'append',' prepend', or + 'overwrite') using the given text. + + RHBZ3 Bug.update() only supports overwriting, so append/prepend + may cause two server roundtrips - one to fetch, and one to update. + ''' + if not which.endswith('_whiteboard'): + which = which + '_whiteboard' + update = {} + if action == 'overwrite': + update[which] = text + else: + r = self._getbug(id) + if which not in r: + raise ValueError, "No such whiteboard %s in bug %s" % \ + (which,str(id)) + wb = r[which] + if action == 'prepend': + update[which] = text+' '+wb + elif action == 'append': + update[which] = wb+' '+text + self._update_bug(id,update) -- cgit From 91d0aef5632d3fc59c97ee9869b20b941e536c65 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Tue, 19 Aug 2008 17:15:03 -0400 Subject: minor comment tweak --- bugzilla/rhbugzilla.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bugzilla/rhbugzilla.py') diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index 647d8bc..beb31df 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -263,7 +263,7 @@ class RHBugzilla(bugzilla.base.BugzillaBase): class RHBugzilla3(Bugzilla32, RHBugzilla): '''Concrete implementation of the Bugzilla protocol. This one uses the - methods provided by Red Hat's Bugzilla 3.1.4+ instance, which is a superset + methods provided by Red Hat's Bugzilla 3.2+ instance, which is a superset of the Bugzilla 3.2 methods. The additional methods (Bug.search, Bug.update) should make their way into a later upstream Bugzilla release (probably 4.0). -- cgit From 2d8243e272243ed0692db47a53f3b939a8440b13 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Fri, 22 Aug 2008 16:28:35 -0400 Subject: replace_getbug_errors_with_None moved to bugzilla.base - fix call --- bugzilla/rhbugzilla.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'bugzilla/rhbugzilla.py') diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index beb31df..978999b 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -85,7 +85,7 @@ class RHBugzilla(bugzilla.base.BugzillaBase): raw_results = mc.run() del mc # check results for xmlrpc errors, and replace them with None - return replace_getbug_errors_with_None(raw_results) + return bugzilla.base.replace_getbug_errors_with_None(raw_results) def _getbugssimple(self,idlist): '''Like _getbugsimple, but takes a list of ids and returns a corresponding list of bug objects. Uses multicall for awesome speed.''' @@ -95,7 +95,7 @@ class RHBugzilla(bugzilla.base.BugzillaBase): raw_results = mc.run() del mc # check results for xmlrpc errors, and replace them with None - return replace_getbug_errors_with_None(raw_results) + return bugzilla.base.replace_getbug_errors_with_None(raw_results) def _query(self,query): '''Query bugzilla and return a list of matching bugs. -- cgit From ed5255764779dbbee7d6fba22d6b24b04e7853bd Mon Sep 17 00:00:00 2001 From: Will Woods Date: Tue, 26 Aug 2008 10:51:48 -0400 Subject: Fix version/useragent for RHBugzilla --- bugzilla/rhbugzilla.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'bugzilla/rhbugzilla.py') diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index 978999b..3cbc83c 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -12,15 +12,16 @@ import bugzilla.base from bugzilla3 import Bugzilla32 -version = '0.2' -user_agent = bugzilla.base.user_agent + ' RHBugzilla/%s' % version - class RHBugzilla(bugzilla.base.BugzillaBase): '''Concrete implementation of the Bugzilla protocol. This one uses the methods provided by Red Hat's Bugzilla 2.18 variant.''' + + version = '0.2' + user_agent = bugzilla.base.user_agent + ' RHBugzilla/%s' % version + def __init__(self,**kwargs): bugzilla.base.BugzillaBase.__init__(self,**kwargs) - self.user_agent = user_agent + self.user_agent = self.__class__.user_agent def _login(self,user,password): '''Backend login method for RHBugzilla.''' @@ -380,7 +381,7 @@ class RHBugzilla3(Bugzilla32, RHBugzilla): raise AttributeError, "Can't find cc list in bug %s" % str(id) self._updatecc(id,r['cc'],'delete') self._updatecc(id,cclist,'add') - # FIXME we don't check inputs on other backend methods, maybe this + # XXX we don't check inputs on other backend methods, maybe this # is more appropriate in the public method(s) else: raise ValueError, "action must be 'add','delete', or 'overwrite'" -- cgit From e22c1f4fbfb268cf4970ae874e8d5988cb5f834f Mon Sep 17 00:00:00 2001 From: Will Woods Date: Tue, 26 Aug 2008 16:21:36 -0400 Subject: Move multicall methods to RHBugzilla, since they're RH-specific --- bugzilla/rhbugzilla.py | 66 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) (limited to 'bugzilla/rhbugzilla.py') diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index 3cbc83c..d7d6295 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -14,7 +14,15 @@ from bugzilla3 import Bugzilla32 class RHBugzilla(bugzilla.base.BugzillaBase): '''Concrete implementation of the Bugzilla protocol. This one uses the - methods provided by Red Hat's Bugzilla 2.18 variant.''' + methods provided by Red Hat's Bugzilla 2.18 variant. + + RHBugzilla supports XMLRPC MultiCall. The methods which start with a + single underscore are thin wrappers around XMLRPC methods and should thus + be safe for multicall use. + + Documentation for most of these methods can be found here: + https://bugzilla.redhat.com/docs/en/html/api/extensions/compat_xmlrpc/code/webservice.html + ''' version = '0.2' user_agent = bugzilla.base.user_agent + ' RHBugzilla/%s' % version @@ -36,6 +44,40 @@ class RHBugzilla(bugzilla.base.BugzillaBase): #---- Methods and properties with basic bugzilla info + def _multicall(self): + '''This returns kind of a mash-up of the Bugzilla object and the + xmlrpclib.MultiCall object. Methods you call on this object will be + added to the MultiCall queue, but they will return None. When you're + ready, call the run() method and all the methods in the queue will be + run and the results of each will be returned in a list. So, for example: + + mc = bz._multicall() + mc._getbug(1) + mc._getbug(1337) + mc._query({'component':'glibc','product':'Fedora','version':'devel'}) + (bug1, bug1337, queryresult) = mc.run() + + Note that you should only use the raw xmlrpc calls (mostly the methods + starting with an underscore). Normal getbug(), for example, tries to + return a Bug object, but with the multicall object it'll end up empty + and, therefore, useless. + + Further note that run() returns a list of raw xmlrpc results; you'll + need to wrap the output in Bug objects yourself if you're doing that + kind of thing. For example, Bugzilla.getbugs() could be implemented: + + mc = self._multicall() + for id in idlist: + mc._getbug(id) + rawlist = mc.run() + return [Bug(self,dict=b) for b in rawlist] + ''' + mc = copy.copy(self) + mc._proxy = xmlrpclib.MultiCall(self._proxy) + def run(): return mc._proxy().results + mc.run = run + return mc + # Connect the backend methods to the XMLRPC methods def _getbugfields(self): return self._proxy.bugzilla.getBugFields() @@ -61,6 +103,28 @@ class RHBugzilla(bugzilla.base.BugzillaBase): if type(product) == int: product = self._product_id_to_name(product) return self._proxy.bugzilla.getProdCompDetails(product) + def _get_info(self,product=None): + '''This is a convenience method that does getqueryinfo, getproducts, + and (optionally) getcomponents in one big fat multicall. This is a bit + faster than calling them all separately. + + If you're doing interactive stuff you should call this, with the + appropriate product name, after connecting to Bugzilla. This will + cache all the info for you and save you an ugly delay later on.''' + mc = self._multicall() + mc._getqueryinfo() + mc._getproducts() + mc._getbugfields() + if product: + mc._getcomponents(product) + mc._getcomponentsdetails(product) + r = mc.run() + (self._querydata,self._querydefaults) = r.pop(0) + self._products = r.pop(0) + self._bugfields = r.pop(0) + if product: + self._components[product] = r.pop(0) + self._components_details[product] = r.pop(0) #---- Methods for reading bugs and bug info -- cgit From 3730239a0cad74fa7ab0a8a69bb12daa79a1174e Mon Sep 17 00:00:00 2001 From: Will Woods Date: Tue, 26 Aug 2008 17:34:21 -0400 Subject: Attempt to support multicall _getbugs in RHBugzilla3 --- bugzilla/rhbugzilla.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) (limited to 'bugzilla/rhbugzilla.py') diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index d7d6295..34b8318 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -332,16 +332,41 @@ class RHBugzilla3(Bugzilla32, RHBugzilla): of the Bugzilla 3.2 methods. The additional methods (Bug.search, Bug.update) should make their way into a later upstream Bugzilla release (probably 4.0). - Note that RHBZ3 *also* supports most of the old RHBZ methods, under the - 'bugzilla' namespace. - + Note that RHBZ3 *also* supports most of the old RHBZ methods, under the + 'bugzilla' namespace, so we use those when BZ3 methods aren't available. + This class was written using bugzilla.redhat.com's API docs: https://bugzilla.redhat.com/docs/en/html/api/ + + By default, _getbugs will multicall Bug.get(id) multiple times, rather than + doing a single Bug.get(idlist) call. You can disable this behavior by + setting the 'multicall' property to False. This is faster, but less + compatible with RHBugzilla. ''' version = '0.1' user_agent = bugzilla.base.user_agent + ' RHBugzilla3/%s' % version + def __init__(self,**kwargs): + Bugzilla32.__init__(self,**kwargs) + self.user_agent = self.__class__.user_agent + self.multicall = kwargs.get('multicall',True) + + def _getbugs(self,idlist): + r = [] + if self.multicall: + mc = self._multicall() + for id in idlist: + mc._proxy.bugzilla.getBug(id) + raw_results = mc.run() + del mc + # check results for xmlrpc errors, and replace them with None + r = bugzilla.base.replace_getbug_errors_with_None(raw_results) + else: + raw_results = self._proxy.Bug.get({'ids':idlist}) + r = [i['internals'] for i in raw_results['bugs']] + return r + def _query(self,query): '''Query bugzilla and return a list of matching bugs. query must be a dict with fields like those in in querydata['fields']. -- cgit From 428fdd43cba0a16dca1068c15f229c29244a09ff Mon Sep 17 00:00:00 2001 From: Will Woods Date: Tue, 26 Aug 2008 17:38:57 -0400 Subject: move "import copy" along with multicall methods --- bugzilla/rhbugzilla.py | 1 + 1 file changed, 1 insertion(+) (limited to 'bugzilla/rhbugzilla.py') diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index 34b8318..b525ed4 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -11,6 +11,7 @@ import bugzilla.base from bugzilla3 import Bugzilla32 +import copy class RHBugzilla(bugzilla.base.BugzillaBase): '''Concrete implementation of the Bugzilla protocol. This one uses the -- cgit From 71bab41bf6068f464827700f68ab7cfbae51e7ab Mon Sep 17 00:00:00 2001 From: Will Woods Date: Tue, 26 Aug 2008 17:52:48 -0400 Subject: Forgot xmlrpclib import --- bugzilla/rhbugzilla.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bugzilla/rhbugzilla.py') diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index b525ed4..cdad54d 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -11,7 +11,7 @@ import bugzilla.base from bugzilla3 import Bugzilla32 -import copy +import copy, xmlrpclib class RHBugzilla(bugzilla.base.BugzillaBase): '''Concrete implementation of the Bugzilla protocol. This one uses the -- cgit From ce753c44a0445b796cd19851cba8dff2e5d455d0 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Thu, 4 Sep 2008 12:28:16 -0400 Subject: Add comments, raise exception if Bugzilla class autodetection fails --- bugzilla/rhbugzilla.py | 1 + 1 file changed, 1 insertion(+) (limited to 'bugzilla/rhbugzilla.py') diff --git a/bugzilla/rhbugzilla.py b/bugzilla/rhbugzilla.py index cdad54d..47a3e22 100644 --- a/bugzilla/rhbugzilla.py +++ b/bugzilla/rhbugzilla.py @@ -353,6 +353,7 @@ class RHBugzilla3(Bugzilla32, RHBugzilla): self.user_agent = self.__class__.user_agent self.multicall = kwargs.get('multicall',True) + # XXX it'd be nice if this wasn't just a copy of RHBugzilla's _getbugs def _getbugs(self,idlist): r = [] if self.multicall: -- cgit