summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/pybugz-0.9.3/bugz/bugzilla.py')
-rw-r--r--third_party/pybugz-0.9.3/bugz/bugzilla.py862
1 files changed, 862 insertions, 0 deletions
diff --git a/third_party/pybugz-0.9.3/bugz/bugzilla.py b/third_party/pybugz-0.9.3/bugz/bugzilla.py
new file mode 100644
index 0000000..957598e
--- /dev/null
+++ b/third_party/pybugz-0.9.3/bugz/bugzilla.py
@@ -0,0 +1,862 @@
+#!/usr/bin/env python
+
+import base64
+import csv
+import getpass
+import locale
+import mimetypes
+import os
+import re
+import sys
+
+from cookielib import LWPCookieJar, CookieJar
+from cStringIO import StringIO
+from urlparse import urlsplit, urljoin
+from urllib import urlencode, quote
+from urllib2 import build_opener, HTTPCookieProcessor, Request
+
+from config import config
+
+from xml.etree import ElementTree
+
+COOKIE_FILE = '.bugz_cookie'
+
+#
+# Return a string truncated to the given length if it is longer.
+#
+
+def ellipsis(text, length):
+ if len(text) > length:
+ return text[:length-4] + "..."
+ else:
+ return text
+
+#
+# HTTP file uploads in Python
+# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306
+#
+
+def post_multipart(host, selector, fields, files):
+ """
+ Post fields and files to an http host as multipart/form-data.
+ fields is a sequence of (name, value) elements for regular form fields.
+ files is a sequence of (name, filename, value) elements for data to be uploaded as files
+ Return the server's response page.
+ """
+ content_type, body = encode_multipart_formdata(fields, files)
+ h = httplib.HTTP(host)
+ h.putrequest('POST', selector)
+ h.putheader('content-type', content_type)
+ h.putheader('content-length', str(len(body)))
+ h.endheaders()
+ h.send(body)
+ errcode, errmsg, headers = h.getreply()
+ return h.file.read()
+
+def encode_multipart_formdata(fields, files):
+ """
+ fields is a sequence of (name, value) elements for regular form fields.
+ files is a sequence of (name, filename, value) elements for data to be uploaded as files
+ Return (content_type, body) ready for httplib.HTTP instance
+ """
+ BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$'
+ CRLF = '\r\n'
+ L = []
+ for (key, value) in fields:
+ L.append('--' + BOUNDARY)
+ L.append('Content-Disposition: form-data; name="%s"' % key)
+ L.append('')
+ L.append(value)
+ for (key, filename, value) in files:
+ L.append('--' + BOUNDARY)
+ L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
+ L.append('Content-Type: %s' % get_content_type(filename))
+ L.append('')
+ L.append(value)
+ L.append('--' + BOUNDARY + '--')
+ L.append('')
+ body = CRLF.join(L)
+ content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
+ return content_type, body
+
+def get_content_type(filename):
+ return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
+
+#
+# Override the behaviour of elementtree and allow us to
+# force the encoding to utf-8
+# Not needed in Python 2.7, since ElementTree.XMLTreeBuilder uses the forced
+# encoding.
+#
+
+class ForcedEncodingXMLTreeBuilder(ElementTree.XMLTreeBuilder):
+ def __init__(self, html = 0, target = None, encoding = None):
+ try:
+ from xml.parsers import expat
+ except ImportError:
+ raise ImportError(
+ "No module named expat; use SimpleXMLTreeBuilder instead"
+ )
+ self._parser = parser = expat.ParserCreate(encoding, "}")
+ if target is None:
+ target = ElementTree.TreeBuilder()
+ self._target = target
+ self._names = {} # name memo cache
+ # callbacks
+ parser.DefaultHandlerExpand = self._default
+ parser.StartElementHandler = self._start
+ parser.EndElementHandler = self._end
+ parser.CharacterDataHandler = self._data
+ # let expat do the buffering, if supported
+ try:
+ self._parser.buffer_text = 1
+ except AttributeError:
+ pass
+ # use new-style attribute handling, if supported
+ try:
+ self._parser.ordered_attributes = 1
+ self._parser.specified_attributes = 1
+ parser.StartElementHandler = self._start_list
+ except AttributeError:
+ pass
+ encoding = None
+ if not parser.returns_unicode:
+ encoding = "utf-8"
+ # target.xml(encoding, None)
+ self._doctype = None
+ self.entity = {}
+
+#
+# Real bugzilla interface
+#
+
+class Bugz:
+ """ Converts sane method calls to Bugzilla HTTP requests.
+
+ @ivar base: base url of bugzilla.
+ @ivar user: username for authenticated operations.
+ @ivar password: password for authenticated operations
+ @ivar cookiejar: for authenticated sessions so we only auth once.
+ @ivar forget: forget user/password after session.
+ @ivar authenticated: is this session authenticated already
+ """
+
+ def __init__(self, base, user = None, password = None, forget = False,
+ skip_auth = False, httpuser = None, httppassword = None ):
+ """
+ {user} and {password} will be prompted if an action needs them
+ and they are not supplied.
+
+ if {forget} is set, the login cookie will be destroyed on quit.
+
+ @param base: base url of the bugzilla
+ @type base: string
+ @keyword user: username for authenticated actions.
+ @type user: string
+ @keyword password: password for authenticated actions.
+ @type password: string
+ @keyword forget: forget login session after termination.
+ @type forget: bool
+ @keyword skip_auth: do not authenticate
+ @type skip_auth: bool
+ """
+ self.base = base
+ scheme, self.host, self.path, query, frag = urlsplit(self.base)
+ self.authenticated = False
+ self.forget = forget
+
+ if not self.forget:
+ try:
+ cookie_file = os.path.join(os.environ['HOME'], COOKIE_FILE)
+ self.cookiejar = LWPCookieJar(cookie_file)
+ if forget:
+ try:
+ self.cookiejar.load()
+ self.cookiejar.clear()
+ self.cookiejar.save()
+ os.chmod(self.cookiejar.filename, 0600)
+ except IOError:
+ pass
+ except KeyError:
+ self.warn('Unable to save session cookies in %s' % cookie_file)
+ self.cookiejar = CookieJar(cookie_file)
+ else:
+ self.cookiejar = CookieJar()
+
+ self.opener = build_opener(HTTPCookieProcessor(self.cookiejar))
+ self.user = user
+ self.password = password
+ self.httpuser = httpuser
+ self.httppassword = httppassword
+ self.skip_auth = skip_auth
+
+ def log(self, status_msg):
+ """Default logging handler. Expected to be overridden by
+ the UI implementing subclass.
+
+ @param status_msg: status message to print
+ @type status_msg: string
+ """
+ return
+
+ def warn(self, warn_msg):
+ """Default logging handler. Expected to be overridden by
+ the UI implementing subclass.
+
+ @param status_msg: status message to print
+ @type status_msg: string
+ """
+ return
+
+ def get_input(self, prompt):
+ """Default input handler. Expected to be override by the
+ UI implementing subclass.
+
+ @param prompt: Prompt message
+ @type prompt: string
+ """
+ return ''
+
+ def auth(self):
+ """Authenticate a session.
+ """
+ # check if we need to authenticate
+ if self.authenticated:
+ return
+
+ # try seeing if we really need to request login
+ if not self.forget:
+ try:
+ self.cookiejar.load()
+ except IOError:
+ pass
+
+ req_url = urljoin(self.base, config.urls['auth'])
+ req_url += '?GoAheadAndLogIn=1'
+ req = Request(req_url, None, config.headers)
+ if self.httpuser and self.httppassword:
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1]
+ req.add_header("Authorization", "Basic %s" % base64string)
+ resp = self.opener.open(req)
+ re_request_login = re.compile(r'<title>.*Log in to .*</title>')
+ if not re_request_login.search(resp.read()):
+ self.log('Already logged in.')
+ self.authenticated = True
+ return
+
+ # prompt for username if we were not supplied with it
+ if not self.user:
+ self.log('No username given.')
+ self.user = self.get_input('Username: ')
+
+ # prompt for password if we were not supplied with it
+ if not self.password:
+ self.log('No password given.')
+ self.password = getpass.getpass()
+
+ # perform login
+ qparams = config.params['auth'].copy()
+ qparams['Bugzilla_login'] = self.user
+ qparams['Bugzilla_password'] = self.password
+ if not self.forget:
+ qparams['Bugzilla_remember'] = 'on'
+
+ req_url = urljoin(self.base, config.urls['auth'])
+ req = Request(req_url, urlencode(qparams), config.headers)
+ if self.httpuser and self.httppassword:
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1]
+ req.add_header("Authorization", "Basic %s" % base64string)
+ resp = self.opener.open(req)
+ if resp.info().has_key('Set-Cookie'):
+ self.authenticated = True
+ if not self.forget:
+ self.cookiejar.save()
+ os.chmod(self.cookiejar.filename, 0600)
+ return True
+ else:
+ raise RuntimeError("Failed to login")
+
+ def extractResults(self, resp):
+ # parse the results into dicts.
+ results = []
+ columns = []
+ rows = []
+
+ for r in csv.reader(resp): rows.append(r)
+ for field in rows[0]:
+ if config.choices['column_alias'].has_key(field):
+ columns.append(config.choices['column_alias'][field])
+ else:
+ self.log('Unknown field: ' + field)
+ columns.append(field)
+ for row in rows[1:]:
+ if "Missing Search" in row[0]:
+ self.log('Bugzilla error (Missing search found)')
+ return None
+ fields = {}
+ for i in range(min(len(row), len(columns))):
+ fields[columns[i]] = row[i]
+ results.append(fields)
+ return results
+
+ def search(self, query, comments = False, order = 'number',
+ assigned_to = None, reporter = None, cc = None,
+ commenter = None, whiteboard = None, keywords = None,
+ status = [], severity = [], priority = [], product = [],
+ component = []):
+ """Search bugzilla for a bug.
+
+ @param query: query string to search in title or {comments}.
+ @type query: string
+ @param order: what order to returns bugs in.
+ @type order: string
+
+ @keyword assigned_to: email address which the bug is assigned to.
+ @type assigned_to: string
+ @keyword reporter: email address matching the bug reporter.
+ @type reporter: string
+ @keyword cc: email that is contained in the CC list
+ @type cc: string
+ @keyword commenter: email of a commenter.
+ @type commenter: string
+
+ @keyword whiteboard: string to search in status whiteboard (gentoo?)
+ @type whiteboard: string
+ @keyword keywords: keyword to search for
+ @type keywords: string
+
+ @keyword status: bug status to match. default is ['NEW', 'ASSIGNED',
+ 'REOPENED'].
+ @type status: list
+ @keyword severity: severity to match, empty means all.
+ @type severity: list
+ @keyword priority: priority levels to patch, empty means all.
+ @type priority: list
+ @keyword comments: search comments instead of just bug title.
+ @type comments: bool
+ @keyword product: search within products. empty means all.
+ @type product: list
+ @keyword component: search within components. empty means all.
+ @type component: list
+
+ @return: list of bugs, each bug represented as a dict
+ @rtype: list of dicts
+ """
+
+ if not self.authenticated and not self.skip_auth:
+ self.auth()
+
+ qparams = config.params['list'].copy()
+ if comments:
+ qparams['long_desc'] = query
+ else:
+ qparams['short_desc'] = query
+
+ qparams['order'] = config.choices['order'].get(order, 'Bug Number')
+ qparams['bug_severity'] = severity or []
+ qparams['priority'] = priority or []
+ if status is None:
+ # NEW, ASSIGNED and REOPENED is obsolete as of bugzilla 3.x and has
+ # been removed from bugs.gentoo.org on 2011/05/01
+ qparams['bug_status'] = ['NEW', 'ASSIGNED', 'REOPENED', 'UNCONFIRMED', 'CONFIRMED', 'IN_PROGRESS']
+ elif [s.upper() for s in status] == ['ALL']:
+ qparams['bug_status'] = config.choices['status']
+ else:
+ qparams['bug_status'] = [s.upper() for s in status]
+ qparams['product'] = product or ''
+ qparams['component'] = component or ''
+ qparams['status_whiteboard'] = whiteboard or ''
+ qparams['keywords'] = keywords or ''
+
+ # hoops to jump through for emails, since there are
+ # only two fields, we have to figure out what combinations
+ # to use if all three are set.
+ unique = list(set([assigned_to, cc, reporter, commenter]))
+ unique = [u for u in unique if u]
+ if len(unique) < 3:
+ for i in range(len(unique)):
+ e = unique[i]
+ n = i + 1
+ qparams['email%d' % n] = e
+ qparams['emailassigned_to%d' % n] = int(e == assigned_to)
+ qparams['emailreporter%d' % n] = int(e == reporter)
+ qparams['emailcc%d' % n] = int(e == cc)
+ qparams['emaillongdesc%d' % n] = int(e == commenter)
+ else:
+ raise AssertionError('Cannot set assigned_to, cc, and '
+ 'reporter in the same query')
+
+ req_params = urlencode(qparams, True)
+ req_url = urljoin(self.base, config.urls['list'])
+ req_url += '?' + req_params
+ req = Request(req_url, None, config.headers)
+ if self.httpuser and self.httppassword:
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1]
+ req.add_header("Authorization", "Basic %s" % base64string)
+ resp = self.opener.open(req)
+ return self.extractResults(resp)
+
+ def namedcmd(self, cmd):
+ """Run command stored in Bugzilla by name.
+
+ @return: Result from the stored command.
+ @rtype: list of dicts
+ """
+
+ if not self.authenticated and not self.skip_auth:
+ self.auth()
+
+ qparams = config.params['namedcmd'].copy()
+ # Is there a better way of getting a command with a space in its name
+ # to be encoded as foo%20bar instead of foo+bar or foo%2520bar?
+ qparams['namedcmd'] = quote(cmd)
+ req_params = urlencode(qparams, True)
+ req_params = req_params.replace('%25','%')
+
+ req_url = urljoin(self.base, config.urls['list'])
+ req_url += '?' + req_params
+ req = Request(req_url, None, config.headers)
+ if self.user and self.password:
+ base64string = base64.encodestring('%s:%s' % (self.user, self.password))[:-1]
+ req.add_header("Authorization", "Basic %s" % base64string)
+ resp = self.opener.open(req)
+
+ return self.extractResults(resp)
+
+ def get(self, bugid):
+ """Get an ElementTree representation of a bug.
+
+ @param bugid: bug id
+ @type bugid: int
+
+ @rtype: ElementTree
+ """
+ if not self.authenticated and not self.skip_auth:
+ self.auth()
+
+ qparams = config.params['show'].copy()
+ qparams['id'] = bugid
+
+ req_params = urlencode(qparams, True)
+ req_url = urljoin(self.base, config.urls['show'])
+ req_url += '?' + req_params
+ req = Request(req_url, None, config.headers)
+ if self.httpuser and self.httppassword:
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1]
+ req.add_header("Authorization", "Basic %s" % base64string)
+ resp = self.opener.open(req)
+
+ data = resp.read()
+ # Get rid of control characters.
+ data = re.sub('[\x00-\x08\x0e-\x1f\x0b\x0c]', '', data)
+ fd = StringIO(data)
+
+ # workaround for ill-defined XML templates in bugzilla 2.20.2
+ (major_version, minor_version) = \
+ (sys.version_info[0], sys.version_info[1])
+ if major_version > 2 or \
+ (major_version == 2 and minor_version >= 7):
+ # If this is 2.7 or greater, then XMLTreeBuilder
+ # does what we want.
+ parser = ElementTree.XMLParser()
+ else:
+ # Running under Python 2.6, so we need to use our
+ # subclass of XMLTreeBuilder instead.
+ parser = ForcedEncodingXMLTreeBuilder(encoding = 'utf-8')
+
+ etree = ElementTree.parse(fd, parser)
+ bug = etree.find('.//bug')
+ if bug is not None and bug.attrib.has_key('error'):
+ return None
+ else:
+ return etree
+
+ def modify(self, bugid, title = None, comment = None, url = None,
+ status = None, resolution = None,
+ assigned_to = None, duplicate = 0,
+ priority = None, severity = None,
+ add_cc = [], remove_cc = [],
+ add_dependson = [], remove_dependson = [],
+ add_blocked = [], remove_blocked = [],
+ whiteboard = None, keywords = None,
+ component = None):
+ """Modify an existing bug
+
+ @param bugid: bug id
+ @type bugid: int
+ @keyword title: new title for bug
+ @type title: string
+ @keyword comment: comment to add
+ @type comment: string
+ @keyword url: new url
+ @type url: string
+ @keyword status: new status (note, if you are changing it to RESOLVED, you need to set {resolution} as well.
+ @type status: string
+ @keyword resolution: new resolution (if status=RESOLVED)
+ @type resolution: string
+ @keyword assigned_to: email (needs to exist in bugzilla)
+ @type assigned_to: string
+ @keyword duplicate: bug id to duplicate against (if resolution = DUPLICATE)
+ @type duplicate: int
+ @keyword priority: new priority for bug
+ @type priority: string
+ @keyword severity: new severity for bug
+ @type severity: string
+ @keyword add_cc: list of emails to add to the cc list
+ @type add_cc: list of strings
+ @keyword remove_cc: list of emails to remove from cc list
+ @type remove_cc: list of string.
+ @keyword add_dependson: list of bug ids to add to the depend list
+ @type add_dependson: list of strings
+ @keyword remove_dependson: list of bug ids to remove from depend list
+ @type remove_dependson: list of strings
+ @keyword add_blocked: list of bug ids to add to the blocked list
+ @type add_blocked: list of strings
+ @keyword remove_blocked: list of bug ids to remove from blocked list
+ @type remove_blocked: list of strings
+
+ @keyword whiteboard: set status whiteboard
+ @type whiteboard: string
+ @keyword keywords: set keywords
+ @type keywords: string
+ @keyword component: set component
+ @type component: string
+
+ @return: list of fields modified.
+ @rtype: list of strings
+ """
+ if not self.authenticated and not self.skip_auth:
+ self.auth()
+
+
+ buginfo = Bugz.get(self, bugid)
+ if not buginfo:
+ return False
+
+ modified = []
+ qparams = config.params['modify'].copy()
+ qparams['id'] = bugid
+ # NOTE: knob has been removed in bugzilla 4 and 3?
+ qparams['knob'] = 'none'
+
+ # copy existing fields
+ FIELDS = ('bug_file_loc', 'bug_severity', 'short_desc', 'bug_status',
+ 'status_whiteboard', 'keywords', 'resolution',
+ 'op_sys', 'priority', 'version', 'target_milestone',
+ 'assigned_to', 'rep_platform', 'product', 'component', 'token')
+
+ FIELDS_MULTI = ('blocked', 'dependson')
+
+ for field in FIELDS:
+ try:
+ qparams[field] = buginfo.find('.//%s' % field).text
+ if qparams[field] is None:
+ del qparams[field]
+ except:
+ pass
+
+ for field in FIELDS_MULTI:
+ qparams[field] = [d.text for d in buginfo.findall('.//%s' % field)
+ if d is not None and d.text is not None]
+
+ # set 'knob' if we are change the status/resolution
+ # or trying to reassign bug.
+ if status:
+ status = status.upper()
+ if resolution:
+ resolution = resolution.upper()
+
+ if status and status != qparams['bug_status']:
+ # Bugzilla >= 3.x
+ qparams['bug_status'] = status
+
+ if status == 'RESOLVED':
+ qparams['knob'] = 'resolve'
+ if resolution:
+ qparams['resolution'] = resolution
+ else:
+ qparams['resolution'] = 'FIXED'
+
+ modified.append(('status', status))
+ modified.append(('resolution', qparams['resolution']))
+ elif status == 'ASSIGNED' or status == 'IN_PROGRESS':
+ qparams['knob'] = 'accept'
+ modified.append(('status', status))
+ elif status == 'REOPENED':
+ qparams['knob'] = 'reopen'
+ modified.append(('status', status))
+ elif status == 'VERIFIED':
+ qparams['knob'] = 'verified'
+ modified.append(('status', status))
+ elif status == 'CLOSED':
+ qparams['knob'] = 'closed'
+ modified.append(('status', status))
+ elif duplicate:
+ # Bugzilla >= 3.x
+ qparams['bug_status'] = "RESOLVED"
+ qparams['resolution'] = "DUPLICATE"
+
+ qparams['knob'] = 'duplicate'
+ qparams['dup_id'] = duplicate
+ modified.append(('status', 'RESOLVED'))
+ modified.append(('resolution', 'DUPLICATE'))
+ elif assigned_to:
+ qparams['knob'] = 'reassign'
+ qparams['assigned_to'] = assigned_to
+ modified.append(('assigned_to', assigned_to))
+
+ # setup modification of other bits
+ if comment:
+ qparams['comment'] = comment
+ modified.append(('comment', ellipsis(comment, 60)))
+ if title:
+ qparams['short_desc'] = title or ''
+ modified.append(('title', title))
+ if url is not None:
+ qparams['bug_file_loc'] = url
+ modified.append(('url', url))
+ if severity is not None:
+ qparams['bug_severity'] = severity
+ modified.append(('severity', severity))
+ if priority is not None:
+ qparams['priority'] = priority
+ modified.append(('priority', priority))
+
+ # cc manipulation
+ if add_cc is not None:
+ qparams['newcc'] = ', '.join(add_cc)
+ modified.append(('newcc', qparams['newcc']))
+ if remove_cc is not None:
+ qparams['cc'] = remove_cc
+ qparams['removecc'] = 'on'
+ modified.append(('cc', remove_cc))
+
+ # bug depend/blocked manipulation
+ changed_dependson = False
+ changed_blocked = False
+ if remove_dependson:
+ for bug_id in remove_dependson:
+ qparams['dependson'].remove(str(bug_id))
+ changed_dependson = True
+ if remove_blocked:
+ for bug_id in remove_blocked:
+ qparams['blocked'].remove(str(bug_id))
+ changed_blocked = True
+ if add_dependson:
+ for bug_id in add_dependson:
+ qparams['dependson'].append(str(bug_id))
+ changed_dependson = True
+ if add_blocked:
+ for bug_id in add_blocked:
+ qparams['blocked'].append(str(bug_id))
+ changed_blocked = True
+
+ qparams['dependson'] = ','.join(qparams['dependson'])
+ qparams['blocked'] = ','.join(qparams['blocked'])
+ if changed_dependson:
+ modified.append(('dependson', qparams['dependson']))
+ if changed_blocked:
+ modified.append(('blocked', qparams['blocked']))
+
+ if whiteboard is not None:
+ qparams['status_whiteboard'] = whiteboard
+ modified.append(('status_whiteboard', whiteboard))
+ if keywords is not None:
+ qparams['keywords'] = keywords
+ modified.append(('keywords', keywords))
+ if component is not None:
+ qparams['component'] = component
+ modified.append(('component', component))
+
+ req_params = urlencode(qparams, True)
+ req_url = urljoin(self.base, config.urls['modify'])
+ req = Request(req_url, req_params, config.headers)
+ if self.httpuser and self.httppassword:
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1]
+ req.add_header("Authorization", "Basic %s" % base64string)
+
+ try:
+ resp = self.opener.open(req)
+ re_error = re.compile(r'id="error_msg".*>([^<]+)<')
+ error = re_error.search(resp.read())
+ if error:
+ print error.group(1)
+ return []
+ return modified
+ except:
+ return []
+
+ def attachment(self, attachid):
+ """Get an attachment by attachment_id
+
+ @param attachid: attachment id
+ @type attachid: int
+
+ @return: dict with three keys, 'filename', 'size', 'fd'
+ @rtype: dict
+ """
+ if not self.authenticated and not self.skip_auth:
+ self.auth()
+
+ qparams = config.params['attach'].copy()
+ qparams['id'] = attachid
+
+ req_params = urlencode(qparams, True)
+ req_url = urljoin(self.base, config.urls['attach'])
+ req_url += '?' + req_params
+ req = Request(req_url, None, config.headers)
+ if self.httpuser and self.httppassword:
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1]
+ req.add_header("Authorization", "Basic %s" % base64string)
+ resp = self.opener.open(req)
+
+ try:
+ content_type = resp.info()['Content-type']
+ namefield = content_type.split(';')[1]
+ filename = re.search(r'name=\"(.*)\"', namefield).group(1)
+ content_length = int(resp.info()['Content-length'], 0)
+ return {'filename': filename, 'size': content_length, 'fd': resp}
+ except:
+ return {}
+
+ def post(self, product, component, title, description, url = '', assigned_to = '', cc = '', keywords = '', version = '', dependson = '', blocked = '', priority = '', severity = ''):
+ """Post a bug
+
+ @param product: product where the bug should be placed
+ @type product: string
+ @param component: component where the bug should be placed
+ @type component: string
+ @param title: title of the bug.
+ @type title: string
+ @param description: description of the bug
+ @type description: string
+ @keyword url: optional url to submit with bug
+ @type url: string
+ @keyword assigned_to: optional email to assign bug to
+ @type assigned_to: string.
+ @keyword cc: option list of CC'd emails
+ @type: string
+ @keyword keywords: option list of bugzilla keywords
+ @type: string
+ @keyword version: version of the component
+ @type: string
+ @keyword dependson: bugs this one depends on
+ @type: string
+ @keyword blocked: bugs this one blocks
+ @type: string
+ @keyword priority: priority of this bug
+ @type: string
+ @keyword severity: severity of this bug
+ @type: string
+
+ @rtype: int
+ @return: the bug number, or 0 if submission failed.
+ """
+ if not self.authenticated and not self.skip_auth:
+ self.auth()
+
+ qparams = config.params['post'].copy()
+ qparams['product'] = product
+ qparams['component'] = component
+ qparams['short_desc'] = title
+ qparams['comment'] = description
+ qparams['assigned_to'] = assigned_to
+ qparams['cc'] = cc
+ qparams['bug_file_loc'] = url
+ qparams['dependson'] = dependson
+ qparams['blocked'] = blocked
+ qparams['keywords'] = keywords
+
+ #XXX: default version is 'unspecified'
+ if version != '':
+ qparams['version'] = version
+
+ #XXX: default priority is 'Normal'
+ if priority != '':
+ qparams['priority'] = priority
+
+ #XXX: default severity is 'normal'
+ if severity != '':
+ qparams['bug_severity'] = severity
+
+ req_params = urlencode(qparams, True)
+ req_url = urljoin(self.base, config.urls['post'])
+ req = Request(req_url, req_params, config.headers)
+ if self.httpuser and self.httppassword:
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1]
+ req.add_header("Authorization", "Basic %s" % base64string)
+ resp = self.opener.open(req)
+
+ try:
+ re_bug = re.compile(r'(?:\s+)?<title>.*Bug ([0-9]+) Submitted.*</title>')
+ bug_match = re_bug.search(resp.read())
+ if bug_match:
+ return int(bug_match.group(1))
+ except:
+ pass
+
+ return 0
+
+ def attach(self, bugid, title, description, filename,
+ content_type = 'text/plain', ispatch = False):
+ """Attach a file to a bug.
+
+ @param bugid: bug id
+ @type bugid: int
+ @param title: short description of attachment
+ @type title: string
+ @param description: long description of the attachment
+ @type description: string
+ @param filename: filename of the attachment
+ @type filename: string
+ @keywords content_type: mime-type of the attachment
+ @type content_type: string
+
+ @rtype: bool
+ @return: True if successful, False if not successful.
+ """
+ if not self.authenticated and not self.skip_auth:
+ self.auth()
+
+ qparams = config.params['attach_post'].copy()
+ qparams['bugid'] = bugid
+ qparams['description'] = title
+ qparams['comment'] = description
+ if ispatch:
+ qparams['ispatch'] = '1'
+ qparams['contenttypeentry'] = 'text/plain'
+ else:
+ qparams['contenttypeentry'] = content_type
+
+ filedata = [('data', filename, open(filename).read())]
+ content_type, body = encode_multipart_formdata(qparams.items(),
+ filedata)
+
+ req_headers = config.headers.copy()
+ req_headers['Content-type'] = content_type
+ req_headers['Content-length'] = len(body)
+ req_url = urljoin(self.base, config.urls['attach_post'])
+ req = Request(req_url, body, req_headers)
+ if self.httpuser and self.httppassword:
+ base64string = base64.encodestring('%s:%s' % (self.httpuser, self.httppassword))[:-1]
+ req.add_header("Authorization", "Basic %s" % base64string)
+ resp = self.opener.open(req)
+
+ # TODO: return attachment id and success?
+ try:
+ re_attach = re.compile(r'<title>(.+)</title>')
+ # Bugzilla 3/4
+ re_attach34 = re.compile(r'Attachment \d+ added to Bug \d+')
+ response = resp.read()
+ attach_match = re_attach.search(response)
+ if attach_match:
+ if attach_match.group(1) == "Changes Submitted" or re_attach34.match(attach_match.group(1)):
+ return True
+ else:
+ return attach_match.group(1)
+ else:
+ return False
+ except:
+ pass
+
+ return False