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/base.py | 843 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 843 insertions(+) create mode 100644 bugzilla/base.py (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py new file mode 100644 index 0000000..6fb8f89 --- /dev/null +++ b/bugzilla/base.py @@ -0,0 +1,843 @@ +# bugzilla.py - a Python interface to bugzilla.redhat.com, using xmlrpclib. +# +# Copyright (C) 2007,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 xmlrpclib, urllib2, cookielib +import os.path, base64, copy + +version = '0.5' +user_agent = 'Python-urllib2/%s bugzilla.py/%s' % \ + (urllib2.__version__,version) + +def replace_getbug_errors_with_None(rawlist): + '''r is a raw xmlrpc response. + If it represents an error, None is returned. + Otherwise, r is returned. + This is mostly used for XMLRPC Multicall handling.''' + # Yes, this is a naive implementation + # XXX: return a generator? + result = [] + for r in rawlist: + if isinstance(r,dict) and 'bug_id' in r: + result.append(r) + else: + result.append(None) + return result + +class BugzillaBase(object): + '''An object which represents the data and methods exported by a Bugzilla + instance. Uses xmlrpclib to do its thing. You'll want to create one thusly: + bz=Bugzilla(url='https://bugzilla.redhat.com/xmlrpc.cgi',user=u,password=p) + + If you so desire, you can use cookie headers for authentication instead. + So you could do: + cf=glob(os.path.expanduser('~/.mozilla/firefox/default.*/cookies.txt')) + bz=Bugzilla(url=url,cookies=cf) + and, assuming you have previously logged info bugzilla with firefox, your + pre-existing auth cookie would be used, thus saving you the trouble of + stuffing your username and password in the bugzilla call. + On the other hand, this currently munges up the cookie so you'll have to + log back in when you next use bugzilla in firefox. So this is not + currently recommended. + + The methods which start with a single underscore are thin wrappers around + xmlrpc calls; those should be safe for multicall usage. + + This is an abstract class; it must be implemented by a concrete subclass + which actually connects the methods provided here to the appropriate + methods on the bugzilla instance. + ''' + def __init__(self,**kwargs): + # Settings the user might want to tweak + self.user = '' + self.password = '' + self.url = '' + self.user_agent = user_agent + # Bugzilla object state info that users shouldn't mess with + self._cookiejar = None + self._proxy = None + self._opener = None + self._querydata = None + self._querydefaults = None + self._products = None + self._bugfields = None + self._components = dict() + self._components_details = dict() + if 'cookies' in kwargs: + self.readcookiefile(kwargs['cookies']) + if 'url' in kwargs: + self.connect(kwargs['url']) + if 'user' in kwargs: + self.user = kwargs['user'] + if 'password' in kwargs: + self.password = kwargs['password'] + + #---- Methods for establishing bugzilla connection and logging in + + def readcookiefile(self,cookiefile): + '''Read the given (Mozilla-style) cookie file and fill in the cookiejar, + allowing us to use the user's saved credentials to access bugzilla.''' + cj = cookielib.MozillaCookieJar() + cj.load(cookiefile) + self._cookiejar = cj + self._cookiejar.filename = cookiefile + + def connect(self,url): + '''Connect to the bugzilla instance with the given url.''' + # Set up the transport + if url.startswith('https'): + self._transport = SafeCookieTransport() + else: + self._transport = CookieTransport() + self._transport.user_agent = self.user_agent + self._transport.cookiejar = self._cookiejar or cookielib.CookieJar() + # Set up the proxy, using the transport + self._proxy = xmlrpclib.ServerProxy(url,self._transport) + # Set up the urllib2 opener (using the same cookiejar) + handler = urllib2.HTTPCookieProcessor(self._cookiejar) + self._opener = urllib2.build_opener(handler) + self._opener.addheaders = [('User-agent',self.user_agent)] + self.url = url + + # Note that the bugzilla methods will ignore an empty user/password if you + # send authentication info as a cookie in the request headers. So it's + # OK if we keep sending empty / bogus login info in other methods. + def login(self,user,password): + '''Attempt to log in using the given username and password. Subsequent + method calls will use this username and password. Returns False if + login fails, otherwise returns a dict of user info. + + Note that it is not required to login before calling other methods; + you may just set user and password and call whatever methods you like. + ''' + self.user = user + self.password = password + try: + r = self._proxy.bugzilla.login(self.user,self.password) + except xmlrpclib.Fault, f: + r = False + return r + + #---- 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 + + def _getbugfields(self): + '''IMPLEMENT ME: Get bugfields from Bugzilla.''' + raise NotImplementedError + def _getqueryinfo(self): + '''IMPLEMENT ME: Get queryinfo from Bugzilla.''' + raise NotImplementedError + def _getproducts(self): + '''IMPLEMENT ME: Get product info from Bugzilla.''' + raise NotImplementedError + def _getcomponentsdetails(self,product): + '''IMPLEMENT ME: get component details for a product''' + raise NotImplementedError + def _getcomponents(self,product): + '''IMPLEMENT ME: Get component dict for a product''' + raise NotImplementedError + + def getbugfields(self,force_refresh=False): + '''Calls getBugFields, which returns a list of fields in each bug + for this bugzilla instance. This can be used to set the list of attrs + on the Bug object.''' + if force_refresh or not self._bugfields: + try: + self._bugfields = self._getbugfields() + except xmlrpclib.Fault, f: + if f.faultCode == 'Client': + # okay, this instance doesn't have getbugfields. fine. + self._bugfields = [] + else: + # something bad actually happened on the server. blow up. + raise f + + return self._bugfields + bugfields = property(fget=lambda self: self.getbugfields(), + fdel=lambda self: setattr(self,'_bugfields',None)) + + def getqueryinfo(self,force_refresh=False): + '''Calls getQueryInfo, which returns a (quite large!) structure that + contains all of the query data and query defaults for the bugzilla + instance. Since this is a weighty call - takes a good 5-10sec on + bugzilla.redhat.com - we load the info in this private method and the + user instead plays with the querydata and querydefaults attributes of + the bugzilla object.''' + # Only fetch the data if we don't already have it, or are forced to + if force_refresh or not (self._querydata and self._querydefaults): + (self._querydata, self._querydefaults) = self._getqueryinfo() + # TODO: map _querydata to a dict, as with _components_details? + return (self._querydata, self._querydefaults) + # Set querydata and querydefaults as properties so they auto-create + # themselves when touched by a user. This bit was lifted from YumBase, + # because skvidal is much smarter than I am. + querydata = property(fget=lambda self: self.getqueryinfo()[0], + fdel=lambda self: setattr(self,"_querydata",None)) + querydefaults = property(fget=lambda self: self.getqueryinfo()[1], + fdel=lambda self: setattr(self,"_querydefaults",None)) + + def getproducts(self,force_refresh=False): + '''Return a dict of product names and product descriptions.''' + if force_refresh or not self._products: + self._products = self._getproducts() + return self._products + # Bugzilla.products is a property - we cache the product list on the first + # call and return it for each subsequent call. + products = property(fget=lambda self: self.getproducts(), + fdel=lambda self: setattr(self,'_products',None)) + + def getcomponents(self,product,force_refresh=False): + '''Return a dict of components:descriptions for the given product.''' + if force_refresh or product not in self._components: + self._components[product] = self._getcomponents(product) + return self._components[product] + # TODO - add a .components property that acts like a dict? + + def getcomponentsdetails(self,product,force_refresh=False): + '''Returns a dict of dicts, containing detailed component information + for the given product. The keys of the dict are component names. For + each component, the value is a dict with the following keys: + description, initialowner, initialqacontact, initialcclist''' + # XXX inconsistent: we don't do this list->dict mapping with querydata + if force_refresh or product not in self._components_details: + clist = self._getcomponentsdetails(product) + cdict = dict() + for item in clist: + name = item['component'] + del item['component'] + cdict[name] = item + self._components_details[product] = cdict + return self._components_details[product] + def getcomponentdetails(self,product,component,force_refresh=False): + '''Get details for a single component. Returns a dict with the + following keys: + description, initialowner, initialqacontact, initialcclist''' + d = self.getcomponentsdetails(product,force_refresh) + return d[component] + + 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 + + def _getbug(self,id): + '''IMPLEMENT ME: Return a dict of full bug info for the given bug id''' + raise NotImplementedError + def _getbugsimple(self,id): + '''IMPLEMENT ME: Return a short dict of simple bug info for the given + bug id''' + raise NotImplementedError + def _query(self,query): + '''IMPLEMENT ME: Query bugzilla and return a list of matching bugs.''' + raise NotImplementedError + + # 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) + + # these return Bug objects + def getbug(self,id): + '''Return a Bug object with the full complement of bug data + already loaded.''' + return Bug(bugzilla=self,dict=self._getbug(id)) + def getbugsimple(self,id): + '''Return a Bug object given bug id, populated with simple info''' + return Bug(bugzilla=self,dict=self._getbugsimple(id)) + def getbugs(self,idlist): + '''Return a list of Bug objects with the full complement of bug data + already loaded. If there's a problem getting the data for a given id, + the corresponding item in the returned list will be None.''' + return [(b and Bug(bugzilla=self,dict=b)) or None for b in self._getbugs(idlist)] + def getbugssimple(self,idlist): + '''Return a list of Bug objects for the given bug ids, populated with + simple info. As with getbugs(), if there's a problem getting the data + for a given bug ID, the corresponding item in the returned list will + be None.''' + return [(b and Bug(bugzilla=self,dict=b)) or None for b in self._getbugssimple(idlist)] + 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 list of Bug objects. + ''' + r = self._query(query) + return [Bug(bugzilla=self,dict=b) for b in r['bugs']] + + def simplequery(self,product,version='',component='',string='',matchtype='allwordssubstr'): + '''Convenience method - query for bugs filed against the given + product, version, and component whose comments match the given string. + matchtype specifies the type of match to be done. matchtype may be + any of the types listed in querydefaults['long_desc_type_list'], e.g.: + ['allwordssubstr','anywordssubstr','substring','casesubstring', + 'allwords','anywords','regexp','notregexp'] + Return value is the same as with query(). + ''' + q = {'product':product,'version':version,'component':component, + 'long_desc':string,'long_desc_type':matchtype} + return self.query(q) + + #---- 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) + + # FIXME inconsistent method signatures + # FIXME add more comments on proper implementation + def _addcomment(self,id,comment,private=False, + timestamp='',worktime='',bz_gid=''): + '''IMPLEMENT ME: add a comment to the given bug ID''' + raise NotImplementedError + def _setstatus(self,id,status,comment='',private=False,private_in_it=False,nomail=False): + '''IMPLEMENT ME: Set the status of the given bug ID''' + raise NotImplementedError + def _closebug(self,id,resolution,dupeid,fixedin,comment,isprivate,private_in_it,nomail): + '''IMPLEMENT ME: close the given bug ID''' + raise NotImplementedError + def _setassignee(self,id,**data): + '''IMPLEMENT ME: set the assignee of the given bug ID''' + raise NotImplementedError + 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): + '''IMPLEMENT ME: Update 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. + ''' + raise NotImplementedError + def _updatewhiteboard(self,id,text,which,action): + '''IMPLEMENT ME: 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.''' + raise NotImplementedError + 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.''' + raise NotImplementedError + + #---- Methods for working with attachments + + def _attachment_encode(self,fh): + '''Return the contents of the file-like object fh in a form + appropriate for attaching to a bug in bugzilla. This is the default + encoding method, base64.''' + # Read data in chunks so we don't end up with two copies of the file + # in RAM. + chunksize = 3072 # base64 encoding wants input in multiples of 3 + data = '' + chunk = fh.read(chunksize) + while chunk: + # we could use chunk.encode('base64') but that throws a newline + # at the end of every output chunk, which increases the size of + # the output. + data = data + base64.b64encode(chunk) + chunk = fh.read(chunksize) + return data + + def _attachfile(self,id,**attachdata): + '''IMPLEMENT ME: attach a file to the given bug. + attachdata MUST contain the following keys: + data: File data, encoded in the bugzilla-preferred format. + attachfile() will encode it with _attachment_encode(). + description: Short description of this attachment. + filename: Filename for the attachment. + The following optional keys may also be added: + comment: An optional comment about this attachment. + isprivate: Set to True if the attachment should be marked private. + ispatch: Set to True if the attachment is a patch. + contenttype: The mime-type of the attached file. Defaults to + application/octet-stream if not set. NOTE that text + files will *not* be viewable in bugzilla unless you + remember to set this to text/plain. So remember that! + Returns (attachment_id,mailresults). + ''' + raise NotImplementedError + + def attachfile(self,id,attachfile,description,**kwargs): + '''Attach a file to the given bug ID. Returns the ID of the attachment + or raises xmlrpclib.Fault if something goes wrong. + attachfile may be a filename (which will be opened) or a file-like + object, which must provide a 'read' method. If it's not one of these, + this method will raise a TypeError. + description is the short description of this attachment. + Optional keyword args are as follows: + filename: this will be used as the filename for the attachment. + REQUIRED if attachfile is a file-like object with no + 'name' attribute, otherwise the filename or .name + attribute will be used. + comment: An optional comment about this attachment. + isprivate: Set to True if the attachment should be marked private. + ispatch: Set to True if the attachment is a patch. + contenttype: The mime-type of the attached file. Defaults to + application/octet-stream if not set. NOTE that text + files will *not* be viewable in bugzilla unless you + remember to set this to text/plain. So remember that! + ''' + if isinstance(attachfile,str): + f = open(attachfile) + elif hasattr(attachfile,'read'): + f = attachfile + else: + raise TypeError, "attachfile must be filename or file-like object" + kwargs['description'] = description + if 'filename' not in kwargs: + kwargs['filename'] = os.path.basename(f.name) + # TODO: guess contenttype? + if 'contenttype' not in kwargs: + kwargs['contenttype'] = 'application/octet-stream' + kwargs['data'] = self._attachment_encode(f) + (attachid, mailresults) = self._attachfile(id,kwargs) + return attachid + + def _attachment_uri(self,attachid): + '''Returns the URI for the given attachment ID.''' + att_uri = self._url.replace('xmlrpc.cgi','attachment.cgi') + att_uri = att_uri + '?%i' % attachid + return att_uri + + def openattachment(self,attachid): + '''Get the contents of the attachment with the given attachment ID. + Returns a file-like object.''' + att_uri = self._attachment_uri(attachid) + att = urllib2.urlopen(att_uri) + # RFC 2183 defines the content-disposition header, if you're curious + disp = att.headers['content-disposition'].split(';') + [filename_parm] = [i for i in disp if i.strip().startswith('filename=')] + (dummy,filename) = filename_parm.split('=') + # RFC 2045/822 defines the grammar for the filename value, but + # I think we just need to remove the quoting. I hope. + att.name = filename.strip('"') + # Hooray, now we have a file-like object with .read() and .name + return att + + #---- createbug - big complicated call to create a new bug + + def _createbug(self,**data): + '''IMPLEMENT ME: Raw xmlrpc call for createBug() + Doesn't bother guessing defaults or checking argument validity. + Returns [bug_id, mailresults]''' + raise NotImplementedError + + def createbug(self,check_args=False,**data): + '''Create a bug with the given info. Returns the bug ID. + data should be given as keyword args - remember that you can also + populate a dict and call createbug(**dict) to fill in keyword args. + The arguments are as follows. Note that some are optional and some + are required. + + "product" => "", + # REQUIRED Name of Bugzilla product. + # Ex: Red Hat Enterprise Linux + "component" => "", + # REQUIRED Name of component in Bugzilla product. + # Ex: anaconda + "version" => "", + # REQUIRED Version in the list for the Bugzilla product. + # Ex: 4.5 + # versions are listed in querydata['product'][]['versions'] + "rep_platform" => "", + # REQUIRED Valid architecture from the rep_platform list. + # Ex: i386 + # See querydefaults['rep_platform_list'] for accepted values. + "bug_severity" => "medium", + # REQUIRED Valid severity from the list of severities. + # See querydefaults['bug_severity_list'] for accepted values. + "op_sys" => "Linux", + # REQUIRED Operating system bug occurs on. + # See querydefaults['op_sys_list'] for accepted values. + "bug_file_loc" => "http://", + # REQUIRED URL to additional information for bug report. + # Ex: http://people.redhat.com/dkl + "short_desc" => "", + # REQUIRED One line summary describing the bug report. + "comment" => "", + # REQUIRED A detail descript about the bug report. + + "alias" => "", + # OPTIONAL Will give the bug an alias name. + # Alias can't be merely numerical. + # Alias can't contain spaces or commas. + # Alias can't be more than 20 chars long. + # Alias has to be unique. + "assigned_to" => "", + # OPTIONAL Will be determined by component owner otherwise. + "reporter" => "", + # OPTIONAL Will use current login if blank. + "qa_contact" => "", + # OPTIONAL Will be determined by component qa_contact otherwise. + "cc" => "", + # OPTIONAL Space or Comma separated list of Bugzilla accounts. + "priority" => "urgent", + # OPTIONAL Valid priority from the list of priorities. + # Ex: medium + # See querydefaults['priority_list'] for accepted values. + "bug_status" => 'NEW', + # OPTIONAL Status to place the new bug in. + # Default: NEW + "blocked" => '', + # OPTIONAL Comma or space separate list of bug id's + # this report blocks. + "dependson" => '', + # OPTIONAL Comma or space separate list of bug id's + # this report depends on. + ''' + required = ('product','component','version','short_desc','comment', + 'rep_platform','bug_severity','op_sys','bug_file_loc') + # The xmlrpc will raise an error if one of these is missing, but + # let's try to save a network roundtrip here if possible.. + for i in required: + if i not in data or not data[i]: + raise TypeError, "required field missing or empty: '%s'" % i + # Sort of a chicken-and-egg problem here - check_args will save you a + # network roundtrip if your op_sys or rep_platform is bad, but at the + # expense of getting querydefaults, which is.. an added network + # roundtrip. Basically it's only useful if you're mucking around with + # createbug() in ipython and you've already loaded querydefaults. + if check_args: + if data['op_sys'] not in self.querydefaults['op_sys_list']: + raise ValueError, "invalid value for op_sys: %s" % data['op_sys'] + if data['rep_platform'] not in self.querydefaults['rep_platform_list']: + raise ValueError, "invalid value for rep_platform: %s" % data['rep_platform'] + # Actually perform the createbug call. + # We return a nearly-empty Bug object, which is kind of a bummer 'cuz + # it'll take another network roundtrip to fill it. We *could* fake it + # and fill in the blanks with the data given to this method, but the + # server might modify/add/drop stuff. Then we'd have a Bug object that + # lied about the actual contents of the database. That would be bad. + [bug_id, mail_results] = self._createbug(**data) + return Bug(self,bug_id=bug_id) + # Trivia: this method has ~5.8 lines of comment per line of code. Yow! + +class CookieTransport(xmlrpclib.Transport): + '''A subclass of xmlrpclib.Transport that supports cookies.''' + cookiejar = None + scheme = 'http' + + # Cribbed from xmlrpclib.Transport.send_user_agent + def send_cookies(self, connection, cookie_request): + if self.cookiejar is None: + self.cookiejar = cookielib.CookieJar() + elif self.cookiejar: + # Let the cookiejar figure out what cookies are appropriate + self.cookiejar.add_cookie_header(cookie_request) + # Pull the cookie headers out of the request object... + cookielist=list() + for h,v in cookie_request.header_items(): + if h.startswith('Cookie'): + cookielist.append([h,v]) + # ...and put them over the connection + for h,v in cookielist: + connection.putheader(h,v) + + # This is the same request() method from xmlrpclib.Transport, + # with a couple additions noted below + def request(self, host, handler, request_body, verbose=0): + h = self.make_connection(host) + if verbose: + h.set_debuglevel(1) + + # ADDED: construct the URL and Request object for proper cookie handling + request_url = "%s://%s/" % (self.scheme,host) + cookie_request = urllib2.Request(request_url) + + self.send_request(h,handler,request_body) + self.send_host(h,host) + self.send_cookies(h,cookie_request) # ADDED. creates cookiejar if None. + self.send_user_agent(h) + self.send_content(h,request_body) + + errcode, errmsg, headers = h.getreply() + + # ADDED: parse headers and get cookies here + # fake a response object that we can fill with the headers above + class CookieResponse: + def __init__(self,headers): self.headers = headers + def info(self): return self.headers + cookie_response = CookieResponse(headers) + # Okay, extract the cookies from the headers + self.cookiejar.extract_cookies(cookie_response,cookie_request) + # And write back any changes + if hasattr(self.cookiejar,'save'): + self.cookiejar.save(self.cookiejar.filename) + + if errcode != 200: + raise xmlrpclib.ProtocolError( + host + handler, + errcode, errmsg, + headers + ) + + self.verbose = verbose + + try: + sock = h._conn.sock + except AttributeError: + sock = None + + return self._parse_response(h.getfile(), sock) + +class SafeCookieTransport(xmlrpclib.SafeTransport,CookieTransport): + '''SafeTransport subclass that supports cookies.''' + scheme = 'https' + request = CookieTransport.request + +class Bug(object): + '''A container object for a bug report. Requires a Bugzilla instance - + every Bug is on a Bugzilla, obviously. + Optional keyword args: + dict=DICT - populate attributes with the result of a getBug() call + bug_id=ID - if dict does not contain bug_id, this is required before + you can read any attributes or make modifications to this + bug. + autorefresh - automatically refresh the data in this bug after calling + a method that modifies the bug. Defaults to True. You can + call refresh() to do this manually. + ''' + def __init__(self,bugzilla,**kwargs): + self.bugzilla = bugzilla + self.autorefresh = True + if 'dict' in kwargs and kwargs['dict']: + self.__dict__.update(kwargs['dict']) + if 'bug_id' in kwargs: + setattr(self,'bug_id',kwargs['bug_id']) + if 'autorefresh' in kwargs: + self.autorefresh = kwargs['autorefresh'] + # No bug_id? this bug is invalid! + if not hasattr(self,'bug_id'): + raise TypeError, "Bug object needs a bug_id" + + self.url = bugzilla.url.replace('xmlrpc.cgi', + 'show_bug.cgi?id=%i' % self.bug_id) + + # TODO: set properties for missing bugfields + # The problem here is that the property doesn't know its own name, + # otherwise we could just do .refresh() and return __dict__[f] after. + # basically I need a suicide property that can replace itself after + # it's called. Or something. + #for f in bugzilla.bugfields: + # if f in self.__dict__: continue + # setattr(self,f,property(fget=lambda self: self.refresh())) + + def __str__(self): + '''Return a simple string representation of this bug''' + # XXX Not really sure why we get short_desc sometimes and + # short_short_desc other times. I feel like I'm working around + # a bug here, so keep an eye on this. + if 'short_short_desc' in self.__dict__: + desc = self.short_short_desc + else: + desc = self.short_desc + return "#%-6s %-10s - %s - %s" % (self.bug_id,self.bug_status, + self.assigned_to,desc) + def __repr__(self): + return '' % (self.bug_id,self.bugzilla.url, + id(self)) + + def __getattr__(self,name): + if 'bug_id' in self.__dict__: + if self.bugzilla.bugfields and name not in self.bugzilla.bugfields: + # We have a list of fields, and you ain't on it. Bail out. + raise AttributeError, "field %s not in bugzilla.bugfields" % name + #print "Bug %i missing %s - loading" % (self.bug_id,name) + self.refresh() + if name in self.__dict__: + return self.__dict__[name] + raise AttributeError, "Bug object has no attribute '%s'" % name + + def refresh(self): + '''Refresh all the data in this Bug.''' + r = self.bugzilla._getbug(self.bug_id) + self.__dict__.update(r) + + def reload(self): + '''An alias for reload()''' + self.refresh() + + def setstatus(self,status,comment='',private=False,private_in_it=False,nomail=False): + '''Update the status for this bug report. + Valid values for status are listed in querydefaults['bug_status_list'] + Commonly-used values are ASSIGNED, MODIFIED, and NEEDINFO. + To change bugs to CLOSED, use .close() instead. + See Bugzilla._setstatus() for details.''' + self.bugzilla._setstatus(self.bug_id,status,comment,private,private_in_it,nomail) + # FIXME reload bug data here + + def setassignee(self,assigned_to='',reporter='',qa_contact='',comment=''): + '''Set any of the assigned_to, reporter, or qa_contact fields to a new + bugzilla account, with an optional comment, e.g. + setassignee(reporter='sadguy@brokencomputer.org', + assigned_to='wwoods@redhat.com') + setassignee(qa_contact='wwoods@redhat.com',comment='wwoods QA ftw') + You must set at least one of the three assignee fields, or this method + will throw a ValueError. + Returns [bug_id, mailresults].''' + if not (assigned_to or reporter or qa_contact): + # XXX is ValueError the right thing to throw here? + raise ValueError, "You must set one of assigned_to, reporter, or qa_contact" + # empty fields are ignored, so it's OK to send 'em + r = self.bugzilla._setassignee(self.bug_id,assigned_to=assigned_to, + reporter=reporter,qa_contact=qa_contact,comment=comment) + # FIXME reload bug data here + return r + def addcomment(self,comment,private=False,timestamp='',worktime='',bz_gid=''): + '''Add the given comment to this bug. Set private to True to mark this + comment as private. You can also set a timestamp for the comment, in + "YYYY-MM-DD HH:MM:SS" form. Worktime is undocumented upstream. + If bz_gid is set, and the entire bug is not already private to that + group, this comment will be private.''' + self.bugzilla._addcomment(self.bug_id,comment,private,timestamp, + worktime,bz_gid) + # FIXME reload bug data here + def close(self,resolution,dupeid=0,fixedin='',comment='',isprivate=False,private_in_it=False,nomail=False): + '''Close this bug. + Valid values for resolution are in bz.querydefaults['resolution_list'] + For bugzilla.redhat.com that's: + ['NOTABUG','WONTFIX','DEFERRED','WORKSFORME','CURRENTRELEASE', + 'RAWHIDE','ERRATA','DUPLICATE','UPSTREAM','NEXTRELEASE','CANTFIX', + 'INSUFFICIENT_DATA'] + If using DUPLICATE, you need to set dupeid to the ID of the other bug. + If using WORKSFORME/CURRENTRELEASE/RAWHIDE/ERRATA/UPSTREAM/NEXTRELEASE + you can (and should) set 'new_fixed_in' to a string representing the + version that fixes the bug. + You can optionally add a comment while closing the bug. Set 'isprivate' + to True if you want that comment to be private. + If you want to suppress sending out mail for this bug closing, set + nomail=True. + ''' + self.bugzilla._closebug(self.bug_id,resolution,dupeid,fixedin, + comment,isprivate,private_in_it,nomail) + # FIXME reload bug data here + def _dowhiteboard(self,text,which,action): + '''Actually does the updateWhiteboard call to perform the given action + (append,prepend,overwrite) with the given text on the given whiteboard + for the given bug.''' + self.bugzilla._updatewhiteboard(self.bug_id,text,which,action) + # FIXME reload bug data here + + def getwhiteboard(self,which='status'): + '''Get the current value of the whiteboard specified by 'which'. + Known whiteboard names: 'status','internal','devel','qa'. + Defaults to the 'status' whiteboard.''' + return getattr(self,"%s_whiteboard" % which) + def appendwhiteboard(self,text,which='status'): + '''Append the given text (with a space before it) to the given + whiteboard. Defaults to using status_whiteboard.''' + self._dowhiteboard(text,which,'append') + def prependwhiteboard(self,text,which='status'): + '''Prepend the given text (with a space following it) to the given + whiteboard. Defaults to using status_whiteboard.''' + self._dowhiteboard(text,which,'prepend') + def setwhiteboard(self,text,which='status'): + '''Overwrites the contents of the given whiteboard with the given text. + Defaults to using status_whiteboard.''' + self._dowhiteboard(text,which,'overwrite') + def addtag(self,tag,which='status'): + '''Adds the given tag to the given bug.''' + whiteboard = self.getwhiteboard(which) + if whiteboard: + self.appendwhiteboard(tag,which) + else: + self.setwhiteboard(tag,which) + def gettags(self,which='status'): + '''Get a list of tags (basically just whitespace-split the given + whiteboard)''' + return self.getwhiteboard(which).split() + def deltag(self,tag,which='status'): + '''Removes the given tag from the given bug.''' + tags = self.gettags(which) + tags.remove(tag) + self.setwhiteboard(' '.join(tags),which) +# TODO: add a sync() method that writes the changed data in the Bug object +# back to Bugzilla. Someday. + +class Bugzilla(object): + '''Magical Bugzilla class that figures out which Bugzilla implementation + to use and uses that.''' + # FIXME STUB + pass -- cgit From c98f4dc5a25b62ab5d0edf33ebadcd89f7fcf75e Mon Sep 17 00:00:00 2001 From: Will Woods Date: Tue, 25 Mar 2008 18:32:44 -0400 Subject: Improve Bugzilla3 class - getbug() works now! I think! --- bugzilla/base.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index 6fb8f89..18da60a 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -713,6 +713,10 @@ class Bug(object): desc = self.short_short_desc else: desc = self.short_desc + # Some BZ3 implementations give us an ID instead of a name. + if 'assigned_to' not in self.__dict__: + if 'assigned_to_id' in self.__dict__: + self.assigned_to = self.bugzilla._getuserforid(self.assigned_to_id) return "#%-6s %-10s - %s - %s" % (self.bug_id,self.bug_status, self.assigned_to,desc) def __repr__(self): -- cgit From 248d7bb3c1fe4c0174bfbcb7e135f9d154f2abe7 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Wed, 26 Mar 2008 18:26:30 -0400 Subject: Add comment for later --- bugzilla/base.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index 18da60a..ad9128e 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -127,6 +127,10 @@ class BugzillaBase(object): #---- Methods and properties with basic bugzilla info + # XXX FIXME Uh-oh. I think MultiCall support is a RHism. + # Even worse, RH's bz3 instance supports the RH methods but *NOT* mc! + # 1) move all multicall-calls into RHBugzilla, and + # 2) either make MC optional, or prefer Bugzilla3 over RHBugzilla 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 -- cgit From 95406dfc9f3518a467c99f03ace1fc57ac1de509 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Fri, 16 May 2008 17:27:36 -0400 Subject: Fix inconsistent comment --- bugzilla/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index ad9128e..e784450 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -509,7 +509,7 @@ class BugzillaBase(object): raise NotImplementedError def createbug(self,check_args=False,**data): - '''Create a bug with the given info. Returns the bug ID. + '''Create a bug with the given info. Returns a new Bug object. data should be given as keyword args - remember that you can also populate a dict and call createbug(**dict) to fill in keyword args. The arguments are as follows. Note that some are optional and some -- cgit From 0f2942820323a254a6237c7f986d90e2d15848d3 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Thu, 5 Jun 2008 13:53:18 -0400 Subject: Quietly fixup missing bug_file_loc --- bugzilla/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index e784450..d2b33e9 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -577,7 +577,10 @@ class BugzillaBase(object): # let's try to save a network roundtrip here if possible.. for i in required: if i not in data or not data[i]: - raise TypeError, "required field missing or empty: '%s'" % i + if i == 'bug_file_loc': + data[i] = 'http://' + else: + raise TypeError, "required field missing or empty: '%s'" % i # Sort of a chicken-and-egg problem here - check_args will save you a # network roundtrip if your op_sys or rep_platform is bad, but at the # expense of getting querydefaults, which is.. an added network -- 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/base.py | 46 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index d2b33e9..1aa2e89 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -11,6 +11,8 @@ import xmlrpclib, urllib2, cookielib import os.path, base64, copy +import logging +log = logging.getLogger('bugzilla') version = '0.5' user_agent = 'Python-urllib2/%s bugzilla.py/%s' % \ @@ -109,10 +111,15 @@ class BugzillaBase(object): # Note that the bugzilla methods will ignore an empty user/password if you # send authentication info as a cookie in the request headers. So it's # OK if we keep sending empty / bogus login info in other methods. + def _login(self,user,password): + '''IMPLEMENT ME: backend login method''' + raise NotImplementedError + def login(self,user,password): '''Attempt to log in using the given username and password. Subsequent method calls will use this username and password. Returns False if - login fails, otherwise returns a dict of user info. + login fails, otherwise returns some kind of login info - typically + either a numeric userid, or a dict of user info. Note that it is not required to login before calling other methods; you may just set user and password and call whatever methods you like. @@ -120,7 +127,7 @@ class BugzillaBase(object): self.user = user self.password = password try: - r = self._proxy.bugzilla.login(self.user,self.password) + r = self._login(self.user,self.password) except xmlrpclib.Fault, f: r = False return r @@ -502,10 +509,14 @@ class BugzillaBase(object): #---- createbug - big complicated call to create a new bug + # Default list of required fields for createbug + createbug_required = ('product','component','version','short_desc','comment', + 'rep_platform','bug_severity','op_sys','bug_file_loc') + def _createbug(self,**data): '''IMPLEMENT ME: Raw xmlrpc call for createBug() Doesn't bother guessing defaults or checking argument validity. - Returns [bug_id, mailresults]''' + Returns bug_id''' raise NotImplementedError def createbug(self,check_args=False,**data): @@ -571,11 +582,9 @@ class BugzillaBase(object): # OPTIONAL Comma or space separate list of bug id's # this report depends on. ''' - required = ('product','component','version','short_desc','comment', - 'rep_platform','bug_severity','op_sys','bug_file_loc') # The xmlrpc will raise an error if one of these is missing, but # let's try to save a network roundtrip here if possible.. - for i in required: + for i in self.createbug_required: if i not in data or not data[i]: if i == 'bug_file_loc': data[i] = 'http://' @@ -597,10 +606,20 @@ class BugzillaBase(object): # and fill in the blanks with the data given to this method, but the # server might modify/add/drop stuff. Then we'd have a Bug object that # lied about the actual contents of the database. That would be bad. - [bug_id, mail_results] = self._createbug(**data) + bug_id = self._createbug(**data) return Bug(self,bug_id=bug_id) # Trivia: this method has ~5.8 lines of comment per line of code. Yow! +class CookieResponse: + '''Fake HTTPResponse object that we can fill with headers we got elsewhere. + We can then pass it to CookieJar.extract_cookies() to make it pull out the + cookies from the set of headers we have.''' + def __init__(self,headers): + self.headers = headers + #log.debug("CookieResponse() headers = %s" % headers) + def info(self): + return self.headers + class CookieTransport(xmlrpclib.Transport): '''A subclass of xmlrpclib.Transport that supports cookies.''' cookiejar = None @@ -609,14 +628,19 @@ class CookieTransport(xmlrpclib.Transport): # Cribbed from xmlrpclib.Transport.send_user_agent def send_cookies(self, connection, cookie_request): if self.cookiejar is None: + log.debug("send_cookies(): creating cookiejar") self.cookiejar = cookielib.CookieJar() elif self.cookiejar: + log.debug("send_cookies(): using existing cookiejar") # Let the cookiejar figure out what cookies are appropriate + log.debug("cookie_request headers currently: %s" % cookie_request.header_items()) self.cookiejar.add_cookie_header(cookie_request) + log.debug("cookie_request headers now: %s" % cookie_request.header_items()) # Pull the cookie headers out of the request object... cookielist=list() for h,v in cookie_request.header_items(): if h.startswith('Cookie'): + log.debug("sending cookie: %s=%s" % (h,v)) cookielist.append([h,v]) # ...and put them over the connection for h,v in cookielist: @@ -630,7 +654,8 @@ class CookieTransport(xmlrpclib.Transport): h.set_debuglevel(1) # ADDED: construct the URL and Request object for proper cookie handling - request_url = "%s://%s/" % (self.scheme,host) + request_url = "%s://%s%s" % (self.scheme,host,handler) + log.debug("request_url is %s" % request_url) cookie_request = urllib2.Request(request_url) self.send_request(h,handler,request_body) @@ -642,13 +667,10 @@ class CookieTransport(xmlrpclib.Transport): errcode, errmsg, headers = h.getreply() # ADDED: parse headers and get cookies here - # fake a response object that we can fill with the headers above - class CookieResponse: - def __init__(self,headers): self.headers = headers - def info(self): return self.headers cookie_response = CookieResponse(headers) # Okay, extract the cookies from the headers self.cookiejar.extract_cookies(cookie_response,cookie_request) + log.debug("cookiejar now contains: %s" % self.cookiejar._cookies) # And write back any changes if hasattr(self.cookiejar,'save'): self.cookiejar.save(self.cookiejar.filename) -- cgit From 1805ac8a6645c3d9ee191e597320d7792f6b91f8 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Fri, 6 Jun 2008 13:23:23 -0400 Subject: remove broken cookiefile junk, add more debugging --- bugzilla/base.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index 1aa2e89..f00bc86 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -72,8 +72,6 @@ class BugzillaBase(object): self._bugfields = None self._components = dict() self._components_details = dict() - if 'cookies' in kwargs: - self.readcookiefile(kwargs['cookies']) if 'url' in kwargs: self.connect(kwargs['url']) if 'user' in kwargs: @@ -83,14 +81,6 @@ class BugzillaBase(object): #---- Methods for establishing bugzilla connection and logging in - def readcookiefile(self,cookiefile): - '''Read the given (Mozilla-style) cookie file and fill in the cookiejar, - allowing us to use the user's saved credentials to access bugzilla.''' - cj = cookielib.MozillaCookieJar() - cj.load(cookiefile) - self._cookiejar = cj - self._cookiejar.filename = cookiefile - def connect(self,url): '''Connect to the bugzilla instance with the given url.''' # Set up the transport @@ -328,6 +318,7 @@ class BugzillaBase(object): def getbug(self,id): '''Return a Bug object with the full complement of bug data already loaded.''' + log.debug("getbug(%i)" % id) return Bug(bugzilla=self,dict=self._getbug(id)) def getbugsimple(self,id): '''Return a Bug object given bug id, populated with simple info''' @@ -645,6 +636,8 @@ class CookieTransport(xmlrpclib.Transport): # ...and put them over the connection for h,v in cookielist: connection.putheader(h,v) + else: + log.debug("send_cookies(): cookiejar empty. Nothing to send.") # This is the same request() method from xmlrpclib.Transport, # with a couple additions noted below @@ -712,14 +705,19 @@ class Bug(object): self.bugzilla = bugzilla self.autorefresh = True if 'dict' in kwargs and kwargs['dict']: + log.debug("Bug(%s)" % kwargs['dict'].keys()) self.__dict__.update(kwargs['dict']) if 'bug_id' in kwargs: + log.debug("Bug(%i)" % kwargs['bug_id']) setattr(self,'bug_id',kwargs['bug_id']) if 'autorefresh' in kwargs: self.autorefresh = kwargs['autorefresh'] # No bug_id? this bug is invalid! if not hasattr(self,'bug_id'): - raise TypeError, "Bug object needs a bug_id" + if hasattr(self,'id'): + self.bug_id = self.id + else: + raise TypeError, "Bug object needs a bug_id" self.url = bugzilla.url.replace('xmlrpc.cgi', 'show_bug.cgi?id=%i' % self.bug_id) -- cgit From 951c16ed6f67be44bfe507d48cd36de49942b9f7 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Fri, 6 Jun 2008 17:01:54 -0400 Subject: Begin adding .bugzillarc support --- bugzilla/base.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index f00bc86..e97cd40 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -1,4 +1,4 @@ -# bugzilla.py - a Python interface to bugzilla.redhat.com, using xmlrpclib. +# base.py - the base classes etc. for a Python interface to bugzilla # # Copyright (C) 2007,2008 Red Hat Inc. # Author: Will Woods @@ -81,6 +81,20 @@ class BugzillaBase(object): #---- Methods for establishing bugzilla connection and logging in + configpath = ['/etc/bugzillarc','~/.bugzillarc'] + def readconfig(self,configpath=None): + '''Read bugzillarc file(s) into memory.''' + configpath = [os.path.expanduser(p) for p in configpath] + import ConfigParser + c = ConfigParser.SafeConfigParser() + if not configpath: + configpath = self.configpath + r = c.read(configpath) + if not r: + return + # FIXME save parsed config in a more lightweight form? + self.conf = c + def connect(self,url): '''Connect to the bugzilla instance with the given url.''' # Set up the transport @@ -97,6 +111,8 @@ class BugzillaBase(object): self._opener = urllib2.build_opener(handler) self._opener.addheaders = [('User-agent',self.user_agent)] self.url = url + # TODO if we have a matching config section for this url, load those + # values now # Note that the bugzilla methods will ignore an empty user/password if you # send authentication info as a cookie in the request headers. So it's -- cgit From 735e18f2d1aa144af541ad972ca419a2b4cef005 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Mon, 9 Jun 2008 14:13:46 -0400 Subject: .bugzillarc support, small fix for bugzilla3 compatibility --- bugzilla/base.py | 58 +++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 13 deletions(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index e97cd40..1ecb790 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -84,16 +84,28 @@ class BugzillaBase(object): configpath = ['/etc/bugzillarc','~/.bugzillarc'] def readconfig(self,configpath=None): '''Read bugzillarc file(s) into memory.''' - configpath = [os.path.expanduser(p) for p in configpath] import ConfigParser - c = ConfigParser.SafeConfigParser() if not configpath: configpath = self.configpath + configpath = [os.path.expanduser(p) for p in configpath] + c = ConfigParser.SafeConfigParser() r = c.read(configpath) if not r: return - # FIXME save parsed config in a more lightweight form? - self.conf = c + # See if we have a config section that matches this url. + section = "" + # Substring match - prefer the longest match found + log.debug("Searching for config section matching %s" % self.url) + for s in sorted(c.sections(), lambda a,b: cmp(len(a),len(b)) or cmp(a,b)): + if s in self.url: + log.debug("Found matching section: %s" % s) + section = s + if not section: + return + for k,v in c.items(section): + if k in ('user','password'): + log.debug("Setting '%s' from configfile" % k) + setattr(self,k,v) def connect(self,url): '''Connect to the bugzilla instance with the given url.''' @@ -111,8 +123,10 @@ class BugzillaBase(object): self._opener = urllib2.build_opener(handler) self._opener.addheaders = [('User-agent',self.user_agent)] self.url = url - # TODO if we have a matching config section for this url, load those - # values now + self.readconfig() # we've changed URLs - reload config + if (self.user and self.password): + log.info("user and password present - doing login()") + self.login() # Note that the bugzilla methods will ignore an empty user/password if you # send authentication info as a cookie in the request headers. So it's @@ -121,17 +135,29 @@ class BugzillaBase(object): '''IMPLEMENT ME: backend login method''' raise NotImplementedError - def login(self,user,password): + def login(self,user=None,password=None): '''Attempt to log in using the given username and password. Subsequent method calls will use this username and password. Returns False if login fails, otherwise returns some kind of login info - typically either a numeric userid, or a dict of user info. - - Note that it is not required to login before calling other methods; - you may just set user and password and call whatever methods you like. + + If user is not set, the value of Bugzilla.user will be used. If *that* + is not set, ValueError will be raised. + + This method will be called implicitly at the end of connect() if user + and password are both set. So under most circumstances you won't need + to call this yourself. ''' - self.user = user - self.password = password + if user: + self.user = user + if password: + self.password = password + + if not self.user: + raise ValueError, "missing username" + if not self.password: + raise ValueError, "missing password" + try: r = self._login(self.user,self.password) except xmlrpclib.Fault, f: @@ -754,8 +780,14 @@ class Bug(object): # a bug here, so keep an eye on this. if 'short_short_desc' in self.__dict__: desc = self.short_short_desc - else: + elif 'short_desc' in self.__dict__: desc = self.short_desc + elif 'summary' in self.__dict__: + desc = self.summary + else: + log.warn("Weird; this bug has no summary?") + desc = "[ERROR: SUMMARY MISSING]" + log.debug(self.__dict__) # Some BZ3 implementations give us an ID instead of a name. if 'assigned_to' not in self.__dict__: if 'assigned_to_id' in self.__dict__: -- 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/base.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index 1ecb790..1be4224 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -62,6 +62,7 @@ class BugzillaBase(object): self.password = '' self.url = '' self.user_agent = user_agent + self.logged_in = False # Bugzilla object state info that users shouldn't mess with self._cookiejar = None self._proxy = None @@ -108,7 +109,14 @@ class BugzillaBase(object): setattr(self,k,v) def connect(self,url): - '''Connect to the bugzilla instance with the given url.''' + '''Connect to the bugzilla instance with the given url. + + This will also read any available config files (see readconfig()), + which may set 'user' and 'password'. + + If 'user' and 'password' are both set, we'll run login(). Otherwise + you'll have to login() yourself before some methods will work. + ''' # Set up the transport if url.startswith('https'): self._transport = SafeCookieTransport() @@ -139,7 +147,8 @@ class BugzillaBase(object): '''Attempt to log in using the given username and password. Subsequent method calls will use this username and password. Returns False if login fails, otherwise returns some kind of login info - typically - either a numeric userid, or a dict of user info. + either a numeric userid, or a dict of user info. It also sets the + logged_in attribute to True, if successful. If user is not set, the value of Bugzilla.user will be used. If *that* is not set, ValueError will be raised. @@ -147,6 +156,7 @@ class BugzillaBase(object): This method will be called implicitly at the end of connect() if user and password are both set. So under most circumstances you won't need to call this yourself. + ''' if user: self.user = user @@ -160,6 +170,9 @@ class BugzillaBase(object): try: r = self._login(self.user,self.password) + self.logged_in = True + log.info("login successful - dropping password from memory") + self.password = '' except xmlrpclib.Fault, f: r = False return r -- 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/base.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index 1be4224..554d89f 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -273,7 +273,13 @@ class BugzillaBase(object): fdel=lambda self: setattr(self,"_querydefaults",None)) def getproducts(self,force_refresh=False): - '''Return a dict of product names and product descriptions.''' + '''Get product data: names, descriptions, etc. + The data varies between Bugzilla versions but the basic format is a + list of dicts, where the dicts will have at least the following keys: + {'id':1,'name':"Some Product",'description':"This is a product"} + + Any method that requires a 'product' can be given either the + id or the name.''' if force_refresh or not self._products: self._products = self._getproducts() return self._products @@ -281,6 +287,17 @@ class BugzillaBase(object): # call and return it for each subsequent call. products = property(fget=lambda self: self.getproducts(), fdel=lambda self: setattr(self,'_products',None)) + def _product_id_to_name(self,productid): + '''Convert a product ID (int) to a product name (str).''' + # This will auto-create the 'products' list + for p in self.products: + if p['id'] == productid: + return p['name'] + def _product_name_to_id(self,product): + '''Convert a product name (str) to a product ID (int).''' + for p in self.products: + if p['name'] == product: + return p['id'] def getcomponents(self,product,force_refresh=False): '''Return a dict of components:descriptions for the given product.''' -- cgit From e423a2a54df23092ec11f890f58b4cb242425cc2 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Mon, 14 Jul 2008 16:41:00 -0400 Subject: comment changes, remove unneeded extra Bugzilla object --- bugzilla/base.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index 554d89f..1ec39d7 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -34,6 +34,7 @@ def replace_getbug_errors_with_None(rawlist): return result class BugzillaBase(object): + # FIXME: remove doc info about cookie handling, add info about .bugzillarc '''An object which represents the data and methods exported by a Bugzilla instance. Uses xmlrpclib to do its thing. You'll want to create one thusly: bz=Bugzilla(url='https://bugzilla.redhat.com/xmlrpc.cgi',user=u,password=p) @@ -409,8 +410,9 @@ class BugzillaBase(object): 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 list of Bug objects. + Also see the _query() method for details about the underlying + implementation. ''' r = self._query(query) return [Bug(bugzilla=self,dict=b) for b in r['bugs']] @@ -943,10 +945,4 @@ class Bug(object): tags.remove(tag) self.setwhiteboard(' '.join(tags),which) # TODO: add a sync() method that writes the changed data in the Bug object -# back to Bugzilla. Someday. - -class Bugzilla(object): - '''Magical Bugzilla class that figures out which Bugzilla implementation - to use and uses that.''' - # FIXME STUB - pass +# back to Bugzilla? -- cgit From 2f5579929fe21479831b378a1dc5f0b561c346c0 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Tue, 5 Aug 2008 15:06:36 -0400 Subject: Add login command and python-bugzilla-specific cookie file. Based on a patch by Zack Cerza. --- bugzilla/base.py | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index 1ec39d7..2a4d56c 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -39,16 +39,14 @@ class BugzillaBase(object): instance. Uses xmlrpclib to do its thing. You'll want to create one thusly: bz=Bugzilla(url='https://bugzilla.redhat.com/xmlrpc.cgi',user=u,password=p) - If you so desire, you can use cookie headers for authentication instead. - So you could do: - cf=glob(os.path.expanduser('~/.mozilla/firefox/default.*/cookies.txt')) - bz=Bugzilla(url=url,cookies=cf) - and, assuming you have previously logged info bugzilla with firefox, your - pre-existing auth cookie would be used, thus saving you the trouble of - stuffing your username and password in the bugzilla call. - On the other hand, this currently munges up the cookie so you'll have to - log back in when you next use bugzilla in firefox. So this is not - currently recommended. + You can get authentication cookies by calling the login() method. These + cookies will be stored in a MozillaCookieJar-style file specified by the + 'cookiefile' attribute (which defaults to ~/.bugzillacookies). + + You can also specify 'user' and 'password' in a bugzillarc file, either + /etc/bugzillarc or ~/.bugzillarc. The latter will override the former. + Be sure to set appropriate permissions on those files if you choose to + store your password in one of them! The methods which start with a single underscore are thin wrappers around xmlrpc calls; those should be safe for multicall usage. @@ -62,6 +60,7 @@ class BugzillaBase(object): self.user = '' self.password = '' self.url = '' + self.cookiefile = os.path.expanduser('~/.bugzillacookies') self.user_agent = user_agent self.logged_in = False # Bugzilla object state info that users shouldn't mess with @@ -83,6 +82,18 @@ class BugzillaBase(object): #---- Methods for establishing bugzilla connection and logging in + def initcookiefile(self,cookiefile=None): + '''Read the given (Mozilla-style) cookie file and fill in the + cookiejar, allowing us to use saved credentials to access Bugzilla. + If no file is given, self.cookiefile will be used.''' + if cookiefile: + self.cookiefile = cookiefile + cj = cookielib.MozillaCookieJar(self.cookiefile) + if os.path.exists(self.cookiefile): + cj.load() + self._cookiejar = cj + self._cookiejar.filename = self.cookiefile + configpath = ['/etc/bugzillarc','~/.bugzillarc'] def readconfig(self,configpath=None): '''Read bugzillarc file(s) into memory.''' @@ -119,12 +130,13 @@ class BugzillaBase(object): you'll have to login() yourself before some methods will work. ''' # Set up the transport + self.initcookiefile() if url.startswith('https'): self._transport = SafeCookieTransport() else: self._transport = CookieTransport() self._transport.user_agent = self.user_agent - self._transport.cookiejar = self._cookiejar or cookielib.CookieJar() + self._transport.cookiejar = self._cookiejar # Set up the proxy, using the transport self._proxy = xmlrpclib.ServerProxy(url,self._transport) # Set up the urllib2 opener (using the same cookiejar) @@ -693,7 +705,7 @@ class CookieTransport(xmlrpclib.Transport): # Cribbed from xmlrpclib.Transport.send_user_agent def send_cookies(self, connection, cookie_request): if self.cookiejar is None: - log.debug("send_cookies(): creating cookiejar") + log.debug("send_cookies(): creating in-memory cookiejar") self.cookiejar = cookielib.CookieJar() elif self.cookiejar: log.debug("send_cookies(): using existing cookiejar") @@ -740,7 +752,11 @@ class CookieTransport(xmlrpclib.Transport): log.debug("cookiejar now contains: %s" % self.cookiejar._cookies) # And write back any changes if hasattr(self.cookiejar,'save'): - self.cookiejar.save(self.cookiejar.filename) + try: + self.cookiejar.save(self.cookiejar.filename) + except e: + log.error("Couldn't write cookiefile %s: %s" % \ + (self.cookiejar.filename,str(e)) if errcode != 200: raise xmlrpclib.ProtocolError( -- cgit From b17845248f58f00b1d4d845775bbd10b5522dc72 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Tue, 5 Aug 2008 15:10:17 -0400 Subject: Fix syntax error typo --- bugzilla/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index 2a4d56c..01eda94 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -756,7 +756,7 @@ class CookieTransport(xmlrpclib.Transport): self.cookiejar.save(self.cookiejar.filename) except e: log.error("Couldn't write cookiefile %s: %s" % \ - (self.cookiejar.filename,str(e)) + (self.cookiejar.filename,str(e))) if errcode != 200: raise xmlrpclib.ProtocolError( -- 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/base.py | 59 +++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 16 deletions(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index 01eda94..6a56fac 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -41,12 +41,20 @@ class BugzillaBase(object): You can get authentication cookies by calling the login() method. These cookies will be stored in a MozillaCookieJar-style file specified by the - 'cookiefile' attribute (which defaults to ~/.bugzillacookies). + 'cookiefile' attribute (which defaults to ~/.bugzillacookies). Once you + get cookies this way, you will be considered logged in until the cookie + expires. - You can also specify 'user' and 'password' in a bugzillarc file, either + You may also specify 'user' and 'password' in a bugzillarc file, either /etc/bugzillarc or ~/.bugzillarc. The latter will override the former. - Be sure to set appropriate permissions on those files if you choose to - store your password in one of them! + The format works like this: + [bugzilla.yoursite.com] + user = username + password = password + You can also use the [DEFAULT] section to set defaults that apply to + any site without a specific section of its own. + Be sure to set appropriate permissions on bugzillarc if you choose to + store your password in it! The methods which start with a single underscore are thin wrappers around xmlrpc calls; those should be safe for multicall usage. @@ -64,8 +72,19 @@ class BugzillaBase(object): self.user_agent = user_agent self.logged_in = False # Bugzilla object state info that users shouldn't mess with + self.init_private_data() + if 'url' in kwargs: + self.connect(kwargs['url']) + if 'user' in kwargs: + self.user = kwargs['user'] + if 'password' in kwargs: + self.password = kwargs['password'] + + def init_private_data(self): + '''initialize private variables used by this bugzilla instance.''' self._cookiejar = None self._proxy = None + self._transport = None self._opener = None self._querydata = None self._querydefaults = None @@ -73,12 +92,6 @@ class BugzillaBase(object): self._bugfields = None self._components = dict() self._components_details = dict() - if 'url' in kwargs: - self.connect(kwargs['url']) - if 'user' in kwargs: - self.user = kwargs['user'] - if 'password' in kwargs: - self.password = kwargs['password'] #---- Methods for establishing bugzilla connection and logging in @@ -130,7 +143,7 @@ class BugzillaBase(object): you'll have to login() yourself before some methods will work. ''' # Set up the transport - self.initcookiefile() + self.initcookiefile() # sets _cookiejar if url.startswith('https'): self._transport = SafeCookieTransport() else: @@ -149,6 +162,10 @@ class BugzillaBase(object): log.info("user and password present - doing login()") self.login() + def disconnect(self): + '''Disconnect from the given bugzilla instance.''' + self.init_private_data() # clears all the connection state + # Note that the bugzilla methods will ignore an empty user/password if you # send authentication info as a cookie in the request headers. So it's # OK if we keep sending empty / bogus login info in other methods. @@ -169,7 +186,6 @@ class BugzillaBase(object): This method will be called implicitly at the end of connect() if user and password are both set. So under most circumstances you won't need to call this yourself. - ''' if user: self.user = user @@ -190,12 +206,23 @@ class BugzillaBase(object): r = False return r + def _logout(self): + '''IMPLEMENT ME: backend login method''' + raise NotImplementedError + + def logout(self): + '''Log out of bugzilla. Drops server connection and user info, and + destroys authentication cookies.''' + self._logout() + self.disconnect() + self.user = '' + self.password = '' + self.logged_in = False + #---- Methods and properties with basic bugzilla info - # XXX FIXME Uh-oh. I think MultiCall support is a RHism. - # Even worse, RH's bz3 instance supports the RH methods but *NOT* mc! - # 1) move all multicall-calls into RHBugzilla, and - # 2) either make MC optional, or prefer Bugzilla3 over RHBugzilla + # XXX FIXME Uh-oh. I think MultiCall support is a RHism. We should probably + # move all multicall-based methods into RHBugzilla. 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 -- 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/base.py | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index 6a56fac..c9eaac9 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -396,36 +396,22 @@ class BugzillaBase(object): def _getbug(self,id): '''IMPLEMENT ME: Return a dict of full bug info for the given bug id''' raise NotImplementedError + def _getbugs(self,idlist): + '''IMPLEMENT ME: Return a list of full bug dicts, one for each of the + given bug ids''' + raise NotImplementedError def _getbugsimple(self,id): '''IMPLEMENT ME: Return a short dict of simple bug info for the given bug id''' raise NotImplementedError + def _getbugssimple(self,idlist): + '''IMPLEMENT ME: Return a list of short bug dicts, one for each of the + given bug ids''' + raise NotImplementedError def _query(self,query): '''IMPLEMENT ME: Query bugzilla and return a list of matching bugs.''' raise NotImplementedError - # 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) - # these return Bug objects def getbug(self,id): '''Return a Bug object with the full complement of bug data -- cgit From 0e070830f198a314fbedebd108de97c738636bd4 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Fri, 15 Aug 2008 10:37:11 -0400 Subject: Let's not skip version 0.4 just yet. --- bugzilla/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index c9eaac9..59c505f 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -14,7 +14,7 @@ import os.path, base64, copy import logging log = logging.getLogger('bugzilla') -version = '0.5' +version = '0.4' user_agent = 'Python-urllib2/%s bugzilla.py/%s' % \ (urllib2.__version__,version) -- 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/base.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index 59c505f..8158998 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -476,10 +476,11 @@ class BugzillaBase(object): def _setassignee(self,id,**data): '''IMPLEMENT ME: set the assignee of the given bug ID''' raise NotImplementedError - def _updatedeps(self,id,deplist): + def _updatedeps(self,id,blocked,dependson,action): '''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')''' + blocked, dependson: list of bug ids/aliases + action: 'add' or 'remove' + ''' raise NotImplementedError def _updatecc(self,id,cclist,action,comment='',nomail=False): '''IMPLEMENT ME: Update the CC list using the action and account list -- 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/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index 8158998..db9b2e9 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -479,16 +479,18 @@ class BugzillaBase(object): def _updatedeps(self,id,blocked,dependson,action): '''IMPLEMENT ME: update the deps (blocked/dependson) for the given bug. blocked, dependson: list of bug ids/aliases - action: 'add' or 'remove' + action: 'add' or 'delete' ''' raise NotImplementedError def _updatecc(self,id,cclist,action,comment='',nomail=False): '''IMPLEMENT ME: Update 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. + Note that using 'overwrite' may result in up to three XMLRPC calls + (fetch list, remove each element, add new elements). Avoid if possible. ''' raise NotImplementedError def _updatewhiteboard(self,id,text,which,action): -- cgit From 7328a5650d39e6d77c0a6b3ad7cb059de4242316 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Mon, 18 Aug 2008 13:40:53 -0400 Subject: Add addcc/deletecc to Bug object --- bugzilla/base.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index db9b2e9..a189e8f 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -976,5 +976,13 @@ class Bug(object): tags = self.gettags(which) tags.remove(tag) self.setwhiteboard(' '.join(tags),which) + def addcc(self,cclist,comment=''): + '''Adds the given email addresses to the CC list for this bug. + cclist: list of email addresses (strings) + comment: optional comment to add to the bug''' + self.bugzilla.updatecc(self.bug_id,cclist,'add',comment) + def deletecc(self,cclist,comment=''): + '''Removes the given email addresses from the CC list for this bug.''' + self.bugzilla.updatecc(self.bug_id,cclist,'delete',comment) # TODO: add a sync() method that writes the changed data in the Bug object # back to Bugzilla? -- cgit From dcbd893cbc7b4104545fcfc1e49fe9f72ed407c4 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Mon, 25 Aug 2008 13:09:41 -0400 Subject: Comment cleanups and extra debugging info --- bugzilla/base.py | 1 + 1 file changed, 1 insertion(+) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index a189e8f..03c8e82 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -984,5 +984,6 @@ class Bug(object): def deletecc(self,cclist,comment=''): '''Removes the given email addresses from the CC list for this bug.''' self.bugzilla.updatecc(self.bug_id,cclist,'delete',comment) +# TODO: attach(), getflag(), setflag() # TODO: add a sync() method that writes the changed data in the Bug object # back to Bugzilla? -- cgit From 2f4456720c01793688ca0ac10546ad3328d6a641 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Tue, 26 Aug 2008 10:51:30 -0400 Subject: Update comments --- bugzilla/base.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index 03c8e82..6b7aa0d 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -34,7 +34,6 @@ def replace_getbug_errors_with_None(rawlist): return result class BugzillaBase(object): - # FIXME: remove doc info about cookie handling, add info about .bugzillarc '''An object which represents the data and methods exported by a Bugzilla instance. Uses xmlrpclib to do its thing. You'll want to create one thusly: bz=Bugzilla(url='https://bugzilla.redhat.com/xmlrpc.cgi',user=u,password=p) @@ -221,8 +220,7 @@ class BugzillaBase(object): #---- Methods and properties with basic bugzilla info - # XXX FIXME Uh-oh. I think MultiCall support is a RHism. We should probably - # move all multicall-based methods into RHBugzilla. + # FIXME MultiCall support is a RHism, so this should move into rhbugzilla 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 @@ -889,7 +887,7 @@ class Bug(object): To change bugs to CLOSED, use .close() instead. See Bugzilla._setstatus() for details.''' self.bugzilla._setstatus(self.bug_id,status,comment,private,private_in_it,nomail) - # FIXME reload bug data here + # TODO reload bug data here? def setassignee(self,assigned_to='',reporter='',qa_contact='',comment=''): '''Set any of the assigned_to, reporter, or qa_contact fields to a new @@ -906,7 +904,7 @@ class Bug(object): # empty fields are ignored, so it's OK to send 'em r = self.bugzilla._setassignee(self.bug_id,assigned_to=assigned_to, reporter=reporter,qa_contact=qa_contact,comment=comment) - # FIXME reload bug data here + # TODO reload bug data here? return r def addcomment(self,comment,private=False,timestamp='',worktime='',bz_gid=''): '''Add the given comment to this bug. Set private to True to mark this @@ -916,7 +914,7 @@ class Bug(object): group, this comment will be private.''' self.bugzilla._addcomment(self.bug_id,comment,private,timestamp, worktime,bz_gid) - # FIXME reload bug data here + # TODO reload bug data here? def close(self,resolution,dupeid=0,fixedin='',comment='',isprivate=False,private_in_it=False,nomail=False): '''Close this bug. Valid values for resolution are in bz.querydefaults['resolution_list'] @@ -935,13 +933,13 @@ class Bug(object): ''' self.bugzilla._closebug(self.bug_id,resolution,dupeid,fixedin, comment,isprivate,private_in_it,nomail) - # FIXME reload bug data here + # TODO reload bug data here? def _dowhiteboard(self,text,which,action): '''Actually does the updateWhiteboard call to perform the given action (append,prepend,overwrite) with the given text on the given whiteboard for the given bug.''' self.bugzilla._updatewhiteboard(self.bug_id,text,which,action) - # FIXME reload bug data here + # TODO reload bug data here? def getwhiteboard(self,which='status'): '''Get the current value of the whiteboard specified by 'which'. @@ -984,6 +982,6 @@ class Bug(object): def deletecc(self,cclist,comment=''): '''Removes the given email addresses from the CC list for this bug.''' self.bugzilla.updatecc(self.bug_id,cclist,'delete',comment) -# TODO: attach(), getflag(), setflag() +# TODO: attach(file), getflag(), setflag() # TODO: add a sync() method that writes the changed data in the Bug object # back to Bugzilla? -- 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/base.py | 61 -------------------------------------------------------- 1 file changed, 61 deletions(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index 6b7aa0d..eb53f46 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -55,9 +55,6 @@ class BugzillaBase(object): Be sure to set appropriate permissions on bugzillarc if you choose to store your password in it! - The methods which start with a single underscore are thin wrappers around - xmlrpc calls; those should be safe for multicall usage. - This is an abstract class; it must be implemented by a concrete subclass which actually connects the methods provided here to the appropriate methods on the bugzilla instance. @@ -220,41 +217,6 @@ class BugzillaBase(object): #---- Methods and properties with basic bugzilla info - # FIXME MultiCall support is a RHism, so this should move into rhbugzilla - 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 - def _getbugfields(self): '''IMPLEMENT ME: Get bugfields from Bugzilla.''' raise NotImplementedError @@ -366,29 +328,6 @@ class BugzillaBase(object): d = self.getcomponentsdetails(product,force_refresh) return d[component] - 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 def _getbug(self,id): -- 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/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index eb53f46..8024736 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -10,7 +10,7 @@ # the full text of the license. import xmlrpclib, urllib2, cookielib -import os.path, base64, copy +import os.path, base64 import logging log = logging.getLogger('bugzilla') -- cgit From 8765e33c2267b1408bd4d11d1bf596ad1d53eb21 Mon Sep 17 00:00:00 2001 From: Will Woods Date: Thu, 4 Sep 2008 12:15:38 -0400 Subject: Create cookiefile with 0600 perms --- bugzilla/base.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'bugzilla/base.py') diff --git a/bugzilla/base.py b/bugzilla/base.py index 8024736..04b7883 100644 --- a/bugzilla/base.py +++ b/bugzilla/base.py @@ -100,6 +100,12 @@ class BugzillaBase(object): cj = cookielib.MozillaCookieJar(self.cookiefile) if os.path.exists(self.cookiefile): cj.load() + else: + # Create an empty file that's only readable by this user + old_umask = os.umask(0077) + f = open(self.cookiefile,"w") + f.close() + os.umask(old_umask) self._cookiejar = cj self._cookiejar.filename = self.cookiefile -- cgit