|
Jeff Mahoney |
3dff52 |
# base.py - the base classes etc. for a Python interface to bugzilla
|
|
Jeff Mahoney |
3dff52 |
#
|
|
Jeff Mahoney |
3dff52 |
# Copyright (C) 2007, 2008, 2009, 2010 Red Hat Inc.
|
|
Jeff Mahoney |
3dff52 |
# Author: Will Woods <wwoods@redhat.com>
|
|
Jeff Mahoney |
3dff52 |
#
|
|
Michal Koutný |
ccf7f1 |
# This work is licensed under the GNU GPLv2 or later.
|
|
Michal Koutný |
ccf7f1 |
# See the COPYING file in the top-level directory.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
import collections
|
|
Jeff Mahoney |
3dff52 |
import getpass
|
|
Jeff Mahoney |
3dff52 |
import locale
|
|
Jeff Mahoney |
3dff52 |
from logging import getLogger
|
|
Michal Koutný |
ccf7f1 |
import mimetypes
|
|
Jeff Mahoney |
3dff52 |
import os
|
|
Jeff Mahoney |
3dff52 |
import sys
|
|
Michal Koutný |
ccf7f1 |
import urllib.parse
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
from io import BytesIO
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
from ._authfiles import _BugzillaRCFile, _BugzillaTokenCache
|
|
Jeff Mahoney |
3dff52 |
from .apiversion import __version__
|
|
Michal Koutný |
ccf7f1 |
from ._backendrest import _BackendREST
|
|
Michal Koutný |
ccf7f1 |
from ._backendxmlrpc import _BackendXMLRPC
|
|
Michal Koutný |
ccf7f1 |
from .bug import Bug, Group, User
|
|
Michal Koutný |
ccf7f1 |
from .exceptions import BugzillaError
|
|
Michal Koutný |
ccf7f1 |
from ._rhconverters import _RHBugzillaConverters
|
|
Michal Koutný |
ccf7f1 |
from ._session import _BugzillaSession
|
|
Michal Koutný |
ccf7f1 |
from ._util import listify
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
log = getLogger(__name__)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def _nested_update(d, u):
|
|
Jeff Mahoney |
3dff52 |
# Helper for nested dict update()
|
|
Jeff Mahoney |
3dff52 |
for k, v in list(u.items()):
|
|
Michal Koutný |
ccf7f1 |
if isinstance(v, collections.abc.Mapping):
|
|
Jeff Mahoney |
3dff52 |
d[k] = _nested_update(d.get(k, {}), v)
|
|
Jeff Mahoney |
3dff52 |
else:
|
|
Jeff Mahoney |
3dff52 |
d[k] = v
|
|
Jeff Mahoney |
3dff52 |
return d
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
class _FieldAlias(object):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
Track API attribute names that differ from what we expose in users.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
For example, originally 'short_desc' was the name of the property that
|
|
Jeff Mahoney |
3dff52 |
maps to 'summary' on modern bugzilla. We want pre-existing API users
|
|
Jeff Mahoney |
3dff52 |
to be able to continue to use Bug.short_desc, and
|
|
Jeff Mahoney |
3dff52 |
query({"short_desc": "foo"}). This class tracks that mapping.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
@oldname: The old attribute name
|
|
Jeff Mahoney |
3dff52 |
@newname: The modern attribute name
|
|
Jeff Mahoney |
3dff52 |
@is_api: If True, use this mapping for values sent to the xmlrpc API
|
|
Jeff Mahoney |
3dff52 |
(like the query example)
|
|
Jeff Mahoney |
3dff52 |
@is_bug: If True, use this mapping for Bug attribute names.
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
def __init__(self, newname, oldname, is_api=True, is_bug=True):
|
|
Jeff Mahoney |
3dff52 |
self.newname = newname
|
|
Jeff Mahoney |
3dff52 |
self.oldname = oldname
|
|
Jeff Mahoney |
3dff52 |
self.is_api = is_api
|
|
Jeff Mahoney |
3dff52 |
self.is_bug = is_bug
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
class _BugzillaAPICache(object):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
Helper class that holds cached API results for things like products,
|
|
Jeff Mahoney |
3dff52 |
components, etc.
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
def __init__(self):
|
|
Jeff Mahoney |
3dff52 |
self.products = []
|
|
Jeff Mahoney |
3dff52 |
self.component_names = {}
|
|
Jeff Mahoney |
3dff52 |
self.bugfields = []
|
|
Michal Koutný |
ccf7f1 |
self.version_raw = None
|
|
Michal Koutný |
ccf7f1 |
self.version_parsed = (0, 0)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
class Bugzilla(object):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
The main API object. Connects to a bugzilla instance over XMLRPC, and
|
|
Jeff Mahoney |
3dff52 |
provides wrapper functions to simplify dealing with API calls.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
The most common invocation here will just be with just a URL:
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
bzapi = Bugzilla("http://bugzilla.example.com")
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
If you have previously logged into that URL, and have cached login
|
|
Michal Koutný |
ccf7f1 |
tokens, you will automatically be logged in. Otherwise to
|
|
Jeff Mahoney |
3dff52 |
log in, you can either pass auth options to __init__, or call a login
|
|
Jeff Mahoney |
3dff52 |
helper like interactive_login().
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
If you are not logged in, you won't be able to access restricted data like
|
|
Jeff Mahoney |
3dff52 |
user email, or perform write actions like bug create/update. But simple
|
|
Jeff Mahoney |
3dff52 |
querys will work correctly.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
If you are unsure if you are logged in, you can check the .logged_in
|
|
Jeff Mahoney |
3dff52 |
property.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
Another way to specify auth credentials is via a 'bugzillarc' file.
|
|
Jeff Mahoney |
3dff52 |
See readconfig() documentation for details.
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
@staticmethod
|
|
Jeff Mahoney |
3dff52 |
def url_to_query(url):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
Given a big huge bugzilla query URL, returns a query dict that can
|
|
Jeff Mahoney |
3dff52 |
be passed along to the Bugzilla.query() method.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
q = {}
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
# pylint: disable=unpacking-non-sequence
|
|
Michal Koutný |
ccf7f1 |
(ignore1, ignore2, path,
|
|
Michal Koutný |
ccf7f1 |
ignore, query, ignore3) = urllib.parse.urlparse(url)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
base = os.path.basename(path)
|
|
Jeff Mahoney |
3dff52 |
if base not in ('buglist.cgi', 'query.cgi'):
|
|
Jeff Mahoney |
3dff52 |
return {}
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
for (k, v) in urllib.parse.parse_qsl(query):
|
|
Jeff Mahoney |
3dff52 |
if k not in q:
|
|
Jeff Mahoney |
3dff52 |
q[k] = v
|
|
Jeff Mahoney |
3dff52 |
elif isinstance(q[k], list):
|
|
Jeff Mahoney |
3dff52 |
q[k].append(v)
|
|
Jeff Mahoney |
3dff52 |
else:
|
|
Jeff Mahoney |
3dff52 |
oldv = q[k]
|
|
Jeff Mahoney |
3dff52 |
q[k] = [oldv, v]
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
# Handle saved searches
|
|
Jeff Mahoney |
3dff52 |
if base == "buglist.cgi" and "namedcmd" in q and "sharer_id" in q:
|
|
Jeff Mahoney |
3dff52 |
q = {
|
|
Jeff Mahoney |
3dff52 |
"sharer_id": q["sharer_id"],
|
|
Jeff Mahoney |
3dff52 |
"savedsearch": q["namedcmd"],
|
|
Jeff Mahoney |
3dff52 |
}
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
return q
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
@staticmethod
|
|
Michal Koutný |
ccf7f1 |
def fix_url(url, force_rest=False):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
Turn passed url into a bugzilla XMLRPC web url
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
:param force_rest: If True, generate a REST API url
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Michal Koutný |
ccf7f1 |
(scheme, netloc, path,
|
|
Michal Koutný |
ccf7f1 |
params, query, fragment) = urllib.parse.urlparse(url)
|
|
Michal Koutný |
ccf7f1 |
if not scheme:
|
|
Michal Koutný |
ccf7f1 |
scheme = 'https'
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
if path and not netloc:
|
|
Michal Koutný |
ccf7f1 |
netloc = path.split("/", 1)[0]
|
|
Michal Koutný |
ccf7f1 |
path = "/".join(path.split("/")[1:]) or None
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
if not path:
|
|
Michal Koutný |
ccf7f1 |
path = 'xmlrpc.cgi'
|
|
Michal Koutný |
ccf7f1 |
if force_rest:
|
|
Michal Koutný |
ccf7f1 |
path = "rest/"
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
newurl = urllib.parse.urlunparse(
|
|
Michal Koutný |
ccf7f1 |
(scheme, netloc, path, params, query, fragment))
|
|
Michal Koutný |
ccf7f1 |
return newurl
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
@staticmethod
|
|
Michal Koutný |
ccf7f1 |
def get_rcfile_default_url():
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Helper to check all the default bugzillarc file paths for
|
|
Michal Koutný |
ccf7f1 |
a [DEFAULT] url=X section, and if found, return it.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
configpaths = _BugzillaRCFile.get_default_configpaths()
|
|
Michal Koutný |
ccf7f1 |
rcfile = _BugzillaRCFile()
|
|
Michal Koutný |
ccf7f1 |
rcfile.set_configpaths(configpaths)
|
|
Michal Koutný |
ccf7f1 |
return rcfile.get_default_url()
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def __init__(self, url=-1, user=None, password=None, cookiefile=-1,
|
|
Jeff Mahoney |
3dff52 |
sslverify=True, tokenfile=-1, use_creds=True, api_key=None,
|
|
Michal Koutný |
ccf7f1 |
cert=None, configpaths=-1,
|
|
Michal Koutný |
ccf7f1 |
force_rest=False, force_xmlrpc=False, requests_session=None):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
:param url: The bugzilla instance URL, which we will connect
|
|
Jeff Mahoney |
3dff52 |
to immediately. Most users will want to specify this at
|
|
Jeff Mahoney |
3dff52 |
__init__ time, but you can defer connecting by passing
|
|
Jeff Mahoney |
3dff52 |
url=None and calling connect(URL) manually
|
|
Jeff Mahoney |
3dff52 |
:param user: optional username to connect with
|
|
Jeff Mahoney |
3dff52 |
:param password: optional password for the connecting user
|
|
Jeff Mahoney |
3dff52 |
:param cert: optional certificate file for client side certificate
|
|
Jeff Mahoney |
3dff52 |
authentication
|
|
Michal Koutný |
ccf7f1 |
:param cookiefile: Deprecated, raises an error if not -1 or None
|
|
Jeff Mahoney |
3dff52 |
:param sslverify: Set this to False to skip SSL hostname and CA
|
|
Jeff Mahoney |
3dff52 |
validation checks, like out of date certificate
|
|
Jeff Mahoney |
3dff52 |
:param tokenfile: Location to cache the API login token so youi
|
|
Jeff Mahoney |
3dff52 |
don't have to keep specifying username/password.
|
|
Jeff Mahoney |
3dff52 |
If -1, use the default path. If None, don't use
|
|
Jeff Mahoney |
3dff52 |
or save any tokenfile.
|
|
Michal Koutný |
ccf7f1 |
:param use_creds: If False, this disables tokenfile
|
|
Michal Koutný |
ccf7f1 |
and configpaths by default. This is a convenience option to
|
|
Michal Koutný |
ccf7f1 |
unset those values at init time. If those values are later
|
|
Michal Koutný |
ccf7f1 |
changed, they may be used for future operations.
|
|
Jeff Mahoney |
3dff52 |
:param sslverify: Maps to 'requests' sslverify parameter. Set to
|
|
Jeff Mahoney |
3dff52 |
False to disable SSL verification, but it can also be a path
|
|
Jeff Mahoney |
3dff52 |
to file or directory for custom certs.
|
|
Michal Koutný |
ccf7f1 |
:param api_key: A bugzilla5+ API key
|
|
Michal Koutný |
ccf7f1 |
:param configpaths: A list of possible bugzillarc locations.
|
|
Michal Koutný |
ccf7f1 |
:param force_rest: Force use of the REST API
|
|
Michal Koutný |
ccf7f1 |
:param force_xmlrpc: Force use of the XMLRPC API. If neither force_X
|
|
Michal Koutný |
ccf7f1 |
parameter are specified, heuristics will be used to determine
|
|
Michal Koutný |
ccf7f1 |
which API to use, with XMLRPC preferred for back compatability.
|
|
Michal Koutný |
ccf7f1 |
:param requests_session: An optional requests.Session object the
|
|
Michal Koutný |
ccf7f1 |
API will use to contact the remote bugzilla instance. This
|
|
Michal Koutný |
ccf7f1 |
way the API user can set up whatever auth bits they may need.
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
if url == -1:
|
|
Jeff Mahoney |
3dff52 |
raise TypeError("Specify a valid bugzilla url, or pass url=None")
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
# Settings the user might want to tweak
|
|
Jeff Mahoney |
3dff52 |
self.user = user or ''
|
|
Jeff Mahoney |
3dff52 |
self.password = password or ''
|
|
Jeff Mahoney |
3dff52 |
self.api_key = api_key
|
|
Michal Koutný |
ccf7f1 |
self.cert = cert or None
|
|
Jeff Mahoney |
3dff52 |
self.url = ''
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
self._backend = None
|
|
Michal Koutný |
ccf7f1 |
self._session = None
|
|
Michal Koutný |
ccf7f1 |
self._user_requests_session = requests_session
|
|
Jeff Mahoney |
3dff52 |
self._sslverify = sslverify
|
|
Jeff Mahoney |
3dff52 |
self._cache = _BugzillaAPICache()
|
|
Jeff Mahoney |
3dff52 |
self._bug_autorefresh = False
|
|
Michal Koutný |
ccf7f1 |
self._is_redhat_bugzilla = False
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
self._rcfile = _BugzillaRCFile()
|
|
Michal Koutný |
ccf7f1 |
self._tokencache = _BugzillaTokenCache()
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
self._force_rest = force_rest
|
|
Michal Koutný |
ccf7f1 |
self._force_xmlrpc = force_xmlrpc
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
if cookiefile not in [None, -1]:
|
|
Michal Koutný |
ccf7f1 |
raise TypeError("cookiefile is deprecated, don't pass any value.")
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
if not use_creds:
|
|
Jeff Mahoney |
3dff52 |
tokenfile = None
|
|
Michal Koutný |
ccf7f1 |
configpaths = []
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
if tokenfile == -1:
|
|
Michal Koutný |
ccf7f1 |
tokenfile = self._tokencache.get_default_path()
|
|
Michal Koutný |
ccf7f1 |
if configpaths == -1:
|
|
Michal Koutný |
ccf7f1 |
configpaths = _BugzillaRCFile.get_default_configpaths()
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
self._settokenfile(tokenfile)
|
|
Michal Koutný |
ccf7f1 |
self._setconfigpath(configpaths)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
if url:
|
|
Jeff Mahoney |
3dff52 |
self.connect(url)
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
def _detect_is_redhat_bugzilla(self):
|
|
Michal Koutný |
ccf7f1 |
if self._is_redhat_bugzilla:
|
|
Michal Koutný |
ccf7f1 |
return True
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
match = ".redhat.com"
|
|
Michal Koutný |
ccf7f1 |
if match in self.url:
|
|
Michal Koutný |
ccf7f1 |
log.info("Using RHBugzilla for URL containing %s", match)
|
|
Michal Koutný |
ccf7f1 |
return True
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
return False
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def _init_class_from_url(self):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
Detect if we should use RHBugzilla class, and if so, set it
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Michal Koutný |
ccf7f1 |
from .oldclasses import RHBugzilla # pylint: disable=cyclic-import
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
if not self._detect_is_redhat_bugzilla():
|
|
Jeff Mahoney |
3dff52 |
return
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
self._is_redhat_bugzilla = True
|
|
Michal Koutný |
ccf7f1 |
if self.__class__ == Bugzilla:
|
|
Michal Koutný |
ccf7f1 |
# Overriding the class doesn't have any functional effect,
|
|
Michal Koutný |
ccf7f1 |
# but we continue to do it for API back compat incase anyone
|
|
Michal Koutný |
ccf7f1 |
# is doing any class comparison. We should drop this in the future
|
|
Michal Koutný |
ccf7f1 |
self.__class__ = RHBugzilla
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
def _get_field_aliases(self):
|
|
Jeff Mahoney |
3dff52 |
# List of field aliases. Maps old style RHBZ parameter
|
|
Jeff Mahoney |
3dff52 |
# names to actual upstream values. Used for createbug() and
|
|
Jeff Mahoney |
3dff52 |
# query include_fields at least.
|
|
Michal Koutný |
ccf7f1 |
ret = []
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
def _add(*args, **kwargs):
|
|
Michal Koutný |
ccf7f1 |
ret.append(_FieldAlias(*args, **kwargs))
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
def _add_both(newname, origname):
|
|
Michal Koutný |
ccf7f1 |
_add(newname, origname, is_api=False)
|
|
Michal Koutný |
ccf7f1 |
_add(origname, newname, is_bug=False)
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
_add('summary', 'short_desc')
|
|
Michal Koutný |
ccf7f1 |
_add('description', 'comment')
|
|
Michal Koutný |
ccf7f1 |
_add('platform', 'rep_platform')
|
|
Michal Koutný |
ccf7f1 |
_add('severity', 'bug_severity')
|
|
Michal Koutný |
ccf7f1 |
_add('status', 'bug_status')
|
|
Michal Koutný |
ccf7f1 |
_add('id', 'bug_id')
|
|
Michal Koutný |
ccf7f1 |
_add('blocks', 'blockedby')
|
|
Michal Koutný |
ccf7f1 |
_add('blocks', 'blocked')
|
|
Michal Koutný |
ccf7f1 |
_add('depends_on', 'dependson')
|
|
Michal Koutný |
ccf7f1 |
_add('creator', 'reporter')
|
|
Michal Koutný |
ccf7f1 |
_add('url', 'bug_file_loc')
|
|
Michal Koutný |
ccf7f1 |
_add('dupe_of', 'dupe_id')
|
|
Michal Koutný |
ccf7f1 |
_add('dupe_of', 'dup_id')
|
|
Michal Koutný |
ccf7f1 |
_add('comments', 'longdescs')
|
|
Michal Koutný |
ccf7f1 |
_add('creation_time', 'opendate')
|
|
Michal Koutný |
ccf7f1 |
_add('creation_time', 'creation_ts')
|
|
Michal Koutný |
ccf7f1 |
_add('whiteboard', 'status_whiteboard')
|
|
Michal Koutný |
ccf7f1 |
_add('last_change_time', 'delta_ts')
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
if self._is_redhat_bugzilla:
|
|
Michal Koutný |
ccf7f1 |
_add_both('fixed_in', 'cf_fixed_in')
|
|
Michal Koutný |
ccf7f1 |
_add_both('qa_whiteboard', 'cf_qa_whiteboard')
|
|
Michal Koutný |
ccf7f1 |
_add_both('devel_whiteboard', 'cf_devel_whiteboard')
|
|
Michal Koutný |
ccf7f1 |
_add_both('internal_whiteboard', 'cf_internal_whiteboard')
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
_add('component', 'components', is_bug=False)
|
|
Michal Koutný |
ccf7f1 |
_add('version', 'versions', is_bug=False)
|
|
Michal Koutný |
ccf7f1 |
# Yes, sub_components is the field name the API expects
|
|
Michal Koutný |
ccf7f1 |
_add('sub_components', 'sub_component', is_bug=False)
|
|
Michal Koutný |
ccf7f1 |
# flags format isn't exactly the same but it's the closest approx
|
|
Michal Koutný |
ccf7f1 |
_add('flags', 'flag_types')
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
return ret
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def _get_user_agent(self):
|
|
Jeff Mahoney |
3dff52 |
return 'python-bugzilla/%s' % __version__
|
|
Jeff Mahoney |
3dff52 |
user_agent = property(_get_user_agent)
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
@property
|
|
Michal Koutný |
ccf7f1 |
def bz_ver_major(self):
|
|
Michal Koutný |
ccf7f1 |
return self._cache.version_parsed[0]
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
@property
|
|
Michal Koutný |
ccf7f1 |
def bz_ver_minor(self):
|
|
Michal Koutný |
ccf7f1 |
return self._cache.version_parsed[1]
|
|
Michal Koutný |
ccf7f1 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
###################
|
|
Jeff Mahoney |
3dff52 |
# Private helpers #
|
|
Jeff Mahoney |
3dff52 |
###################
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
def _get_version(self):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Michal Koutný |
ccf7f1 |
Return version number as a float
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Michal Koutný |
ccf7f1 |
return float("%d.%d" % (self.bz_ver_major, self.bz_ver_minor))
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def _get_bug_aliases(self):
|
|
Jeff Mahoney |
3dff52 |
return [(f.newname, f.oldname)
|
|
Michal Koutný |
ccf7f1 |
for f in self._get_field_aliases() if f.is_bug]
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def _get_api_aliases(self):
|
|
Jeff Mahoney |
3dff52 |
return [(f.newname, f.oldname)
|
|
Michal Koutný |
ccf7f1 |
for f in self._get_field_aliases() if f.is_api]
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
#################
|
|
Michal Koutný |
ccf7f1 |
# Auth handling #
|
|
Michal Koutný |
ccf7f1 |
#################
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def _getcookiefile(self):
|
|
Michal Koutný |
ccf7f1 |
return None
|
|
Michal Koutný |
ccf7f1 |
cookiefile = property(_getcookiefile)
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
def _gettokenfile(self):
|
|
Michal Koutný |
ccf7f1 |
return self._tokencache.get_filename()
|
|
Michal Koutný |
ccf7f1 |
def _settokenfile(self, filename):
|
|
Michal Koutný |
ccf7f1 |
self._tokencache.set_filename(filename)
|
|
Michal Koutný |
ccf7f1 |
def _deltokenfile(self):
|
|
Michal Koutný |
ccf7f1 |
self._settokenfile(None)
|
|
Michal Koutný |
ccf7f1 |
tokenfile = property(_gettokenfile, _settokenfile, _deltokenfile)
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
def _getconfigpath(self):
|
|
Michal Koutný |
ccf7f1 |
return self._rcfile.get_configpaths()
|
|
Michal Koutný |
ccf7f1 |
def _setconfigpath(self, configpaths):
|
|
Michal Koutný |
ccf7f1 |
return self._rcfile.set_configpaths(configpaths)
|
|
Michal Koutný |
ccf7f1 |
def _delconfigpath(self):
|
|
Michal Koutný |
ccf7f1 |
return self._rcfile.set_configpaths(None)
|
|
Michal Koutný |
ccf7f1 |
configpath = property(_getconfigpath, _setconfigpath, _delconfigpath)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
#############################
|
|
Jeff Mahoney |
3dff52 |
# Login/connection handling #
|
|
Jeff Mahoney |
3dff52 |
#############################
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
def readconfig(self, configpath=None, overwrite=True):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
:param configpath: Optional bugzillarc path to read, instead of
|
|
Jeff Mahoney |
3dff52 |
the default list.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
This function is called automatically from Bugzilla connect(), which
|
|
Jeff Mahoney |
3dff52 |
is called at __init__ if a URL is passed. Calling it manually is
|
|
Jeff Mahoney |
3dff52 |
just for passing in a non-standard configpath.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
The locations for the bugzillarc file are preferred in this order:
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
~/.config/python-bugzilla/bugzillarc
|
|
Jeff Mahoney |
3dff52 |
~/.bugzillarc
|
|
Jeff Mahoney |
3dff52 |
/etc/bugzillarc
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
It has content like:
|
|
Jeff Mahoney |
3dff52 |
[bugzilla.yoursite.com]
|
|
Jeff Mahoney |
3dff52 |
user = username
|
|
Jeff Mahoney |
3dff52 |
password = password
|
|
Jeff Mahoney |
3dff52 |
Or
|
|
Jeff Mahoney |
3dff52 |
[bugzilla.yoursite.com]
|
|
Jeff Mahoney |
3dff52 |
api_key = key
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
The file can have multiple sections for different bugzilla instances.
|
|
Jeff Mahoney |
3dff52 |
A 'url' field in the [DEFAULT] section can be used to set a default
|
|
Jeff Mahoney |
3dff52 |
URL for the bugzilla command line tool.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
Be sure to set appropriate permissions on bugzillarc if you choose to
|
|
Jeff Mahoney |
3dff52 |
store your password in it!
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
:param overwrite: If True, bugzillarc will clobber any already
|
|
Michal Koutný |
ccf7f1 |
set self.user/password/api_key/cert value.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
if configpath:
|
|
Michal Koutný |
ccf7f1 |
self._setconfigpath(configpath)
|
|
Michal Koutný |
ccf7f1 |
data = self._rcfile.parse(self.url)
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
for key, val in data.items():
|
|
Michal Koutný |
ccf7f1 |
if key == "api_key" and (overwrite or not self.api_key):
|
|
Jeff Mahoney |
3dff52 |
log.debug("bugzillarc: setting api_key")
|
|
Jeff Mahoney |
3dff52 |
self.api_key = val
|
|
Michal Koutný |
ccf7f1 |
elif key == "user" and (overwrite or not self.user):
|
|
Jeff Mahoney |
3dff52 |
log.debug("bugzillarc: setting user=%s", val)
|
|
Jeff Mahoney |
3dff52 |
self.user = val
|
|
Michal Koutný |
ccf7f1 |
elif key == "password" and (overwrite or not self.password):
|
|
Jeff Mahoney |
3dff52 |
log.debug("bugzillarc: setting password")
|
|
Jeff Mahoney |
3dff52 |
self.password = val
|
|
Michal Koutný |
ccf7f1 |
elif key == "cert" and (overwrite or not self.cert):
|
|
Jeff Mahoney |
3dff52 |
log.debug("bugzillarc: setting cert")
|
|
Jeff Mahoney |
3dff52 |
self.cert = val
|
|
Jeff Mahoney |
3dff52 |
else:
|
|
Jeff Mahoney |
3dff52 |
log.debug("bugzillarc: unknown key=%s", key)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def _set_bz_version(self, version):
|
|
Michal Koutný |
ccf7f1 |
self._cache.version_raw = version
|
|
Jeff Mahoney |
3dff52 |
try:
|
|
Michal Koutný |
ccf7f1 |
major, minor = [int(i) for i in version.split(".")[0:2]]
|
|
Jeff Mahoney |
3dff52 |
except Exception:
|
|
Jeff Mahoney |
3dff52 |
log.debug("version doesn't match expected format X.Y.Z, "
|
|
Jeff Mahoney |
3dff52 |
"assuming 5.0", exc_info=True)
|
|
Michal Koutný |
ccf7f1 |
major = 5
|
|
Michal Koutný |
ccf7f1 |
minor = 0
|
|
Michal Koutný |
ccf7f1 |
self._cache.version_parsed = (major, minor)
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
def _get_backend_class(self, url): # pragma: no cover
|
|
Michal Koutný |
ccf7f1 |
# This is a hook for the test suite to do some mock hackery
|
|
Michal Koutný |
ccf7f1 |
if self._force_rest and self._force_xmlrpc:
|
|
Michal Koutný |
ccf7f1 |
raise BugzillaError(
|
|
Michal Koutný |
ccf7f1 |
"Cannot specify both force_rest and force_xmlrpc")
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
xmlurl = self.fix_url(url)
|
|
Michal Koutný |
ccf7f1 |
if self._force_xmlrpc:
|
|
Michal Koutný |
ccf7f1 |
return _BackendXMLRPC, xmlurl
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
resturl = self.fix_url(url, force_rest=self._force_rest)
|
|
Michal Koutný |
ccf7f1 |
if self._force_rest:
|
|
Michal Koutný |
ccf7f1 |
return _BackendREST, resturl
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
# Simple heuristic if the original url has a path in it
|
|
Michal Koutný |
ccf7f1 |
if "/xmlrpc" in url:
|
|
Michal Koutný |
ccf7f1 |
return _BackendXMLRPC, xmlurl
|
|
Michal Koutný |
ccf7f1 |
if "/rest" in url:
|
|
Michal Koutný |
ccf7f1 |
return _BackendREST, resturl
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
# We were passed something like bugzilla.example.com but we
|
|
Michal Koutný |
ccf7f1 |
# aren't sure which method to use, try probing
|
|
Michal Koutný |
ccf7f1 |
if _BackendXMLRPC.probe(xmlurl):
|
|
Michal Koutný |
ccf7f1 |
return _BackendXMLRPC, xmlurl
|
|
Michal Koutný |
ccf7f1 |
if _BackendREST.probe(resturl):
|
|
Michal Koutný |
ccf7f1 |
return _BackendREST, resturl
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
# Otherwise fallback to XMLRPC default and let it fail
|
|
Michal Koutný |
ccf7f1 |
return _BackendXMLRPC, xmlurl
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def connect(self, url=None):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
Connect to the bugzilla instance with the given url. This is
|
|
Jeff Mahoney |
3dff52 |
called by __init__ if a URL is passed. Or it can be called manually
|
|
Jeff Mahoney |
3dff52 |
at any time with a passed URL.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
This will also read any available config files (see readconfig()),
|
|
Jeff Mahoney |
3dff52 |
which may set 'user' and 'password', and others.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
If 'user' and 'password' are both set, we'll run login(). Otherwise
|
|
Jeff Mahoney |
3dff52 |
you'll have to login() yourself before some methods will work.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
if self._session:
|
|
Jeff Mahoney |
3dff52 |
self.disconnect()
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
url = url or self.url
|
|
Michal Koutný |
ccf7f1 |
backendclass, newurl = self._get_backend_class(url)
|
|
Michal Koutný |
ccf7f1 |
if url != newurl:
|
|
Michal Koutný |
ccf7f1 |
log.debug("Converted url=%s to fixed url=%s", url, newurl)
|
|
Michal Koutný |
ccf7f1 |
self.url = newurl
|
|
Michal Koutný |
ccf7f1 |
log.debug("Connecting with URL %s", self.url)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
# we've changed URLs - reload config
|
|
Michal Koutný |
ccf7f1 |
self.readconfig(overwrite=False)
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
# Detect if connecting to redhat bugzilla
|
|
Michal Koutný |
ccf7f1 |
self._init_class_from_url()
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
self._session = _BugzillaSession(self.url, self.user_agent,
|
|
Michal Koutný |
ccf7f1 |
sslverify=self._sslverify,
|
|
Michal Koutný |
ccf7f1 |
cert=self.cert,
|
|
Michal Koutný |
ccf7f1 |
tokencache=self._tokencache,
|
|
Michal Koutný |
ccf7f1 |
api_key=self.api_key,
|
|
Michal Koutný |
ccf7f1 |
is_redhat_bugzilla=self._is_redhat_bugzilla,
|
|
Michal Koutný |
ccf7f1 |
requests_session=self._user_requests_session)
|
|
Michal Koutný |
ccf7f1 |
self._backend = backendclass(self.url, self._session)
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
if (self.user and self.password):
|
|
Jeff Mahoney |
3dff52 |
log.info("user and password present - doing login()")
|
|
Jeff Mahoney |
3dff52 |
self.login()
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
if self.api_key:
|
|
Jeff Mahoney |
3dff52 |
log.debug("using API key")
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
version = self._backend.bugzilla_version()["version"]
|
|
Jeff Mahoney |
3dff52 |
log.debug("Bugzilla version string: %s", version)
|
|
Jeff Mahoney |
3dff52 |
self._set_bz_version(version)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
@property
|
|
Michal Koutný |
ccf7f1 |
def _proxy(self):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Return an xmlrpc ServerProxy instance that will work seamlessly
|
|
Michal Koutný |
ccf7f1 |
with bugzilla
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
Some apps have historically accessed _proxy directly, like
|
|
Michal Koutný |
ccf7f1 |
fedora infrastrucutre pieces. So we consider it part of the API
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
return self._backend.get_xmlrpc_proxy()
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
def is_xmlrpc(self):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
:returns: True if using the XMLRPC API
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
return self._backend.is_xmlrpc()
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
def is_rest(self):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
:returns: True if using the REST API
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
return self._backend.is_rest()
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
def get_requests_session(self):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Give API users access to the Requests.session object we use for
|
|
Michal Koutný |
ccf7f1 |
talking to the remote bugzilla instance.
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
:returns: The Requests.session object backing the open connection.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
return self._session.get_requests_session()
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
def disconnect(self):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Disconnect from the given bugzilla instance.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
self._backend = None
|
|
Michal Koutný |
ccf7f1 |
self._session = None
|
|
Michal Koutný |
ccf7f1 |
self._cache = _BugzillaAPICache()
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
def login(self, user=None, password=None, restrict_login=None):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Attempt to log in using the given username and password. Subsequent
|
|
Jeff Mahoney |
3dff52 |
method calls will use this username and password. Returns False if
|
|
Jeff Mahoney |
3dff52 |
login fails, otherwise returns some kind of login info - typically
|
|
Jeff Mahoney |
3dff52 |
either a numeric userid, or a dict of user info.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
If user is not set, the value of Bugzilla.user will be used. If *that*
|
|
Jeff Mahoney |
3dff52 |
is not set, ValueError will be raised. If login fails, BugzillaError
|
|
Jeff Mahoney |
3dff52 |
will be raised.
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
The login session can be restricted to current user IP address
|
|
Michal Koutný |
ccf7f1 |
with restrict_login argument. (Bugzilla 4.4+)
|
|
Michal Koutný |
ccf7f1 |
|
|
Jeff Mahoney |
3dff52 |
This method will be called implicitly at the end of connect() if user
|
|
Jeff Mahoney |
3dff52 |
and password are both set. So under most circumstances you won't need
|
|
Jeff Mahoney |
3dff52 |
to call this yourself.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
if self.api_key:
|
|
Jeff Mahoney |
3dff52 |
raise ValueError("cannot login when using an API key")
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
if user:
|
|
Jeff Mahoney |
3dff52 |
self.user = user
|
|
Jeff Mahoney |
3dff52 |
if password:
|
|
Jeff Mahoney |
3dff52 |
self.password = password
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
if not self.user:
|
|
Jeff Mahoney |
3dff52 |
raise ValueError("missing username")
|
|
Jeff Mahoney |
3dff52 |
if not self.password:
|
|
Jeff Mahoney |
3dff52 |
raise ValueError("missing password")
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
payload = {"login": self.user}
|
|
Michal Koutný |
ccf7f1 |
if restrict_login:
|
|
Michal Koutný |
ccf7f1 |
payload['restrict_login'] = True
|
|
Michal Koutný |
ccf7f1 |
log.debug("logging in with options %s", str(payload))
|
|
Michal Koutný |
ccf7f1 |
payload['password'] = self.password
|
|
Michal Koutný |
ccf7f1 |
|
|
Jeff Mahoney |
3dff52 |
try:
|
|
Michal Koutný |
ccf7f1 |
ret = self._backend.user_login(payload)
|
|
Jeff Mahoney |
3dff52 |
self.password = ''
|
|
Michal Koutný |
ccf7f1 |
log.info("login succeeded for user=%s", self.user)
|
|
Michal Koutný |
ccf7f1 |
if "token" in ret:
|
|
Michal Koutný |
ccf7f1 |
self._tokencache.set_value(self.url, ret["token"])
|
|
Jeff Mahoney |
3dff52 |
return ret
|
|
Michal Koutný |
ccf7f1 |
except Exception as e:
|
|
Michal Koutný |
ccf7f1 |
log.debug("Login exception: %s", str(e), exc_info=True)
|
|
Michal Koutný |
ccf7f1 |
raise BugzillaError("Login failed: %s" %
|
|
Michal Koutný |
ccf7f1 |
BugzillaError.get_bugzilla_error_string(e)) from None
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
def interactive_save_api_key(self):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Helper method to interactively ask for an API key, verify it
|
|
Michal Koutný |
ccf7f1 |
is valid, and save it to a bugzillarc file referenced via
|
|
Michal Koutný |
ccf7f1 |
self.configpaths
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
sys.stdout.write('API Key: ')
|
|
Michal Koutný |
ccf7f1 |
sys.stdout.flush()
|
|
Michal Koutný |
ccf7f1 |
api_key = sys.stdin.readline().strip()
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
self.disconnect()
|
|
Michal Koutný |
ccf7f1 |
self.api_key = api_key
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
log.info('Checking API key... ')
|
|
Michal Koutný |
ccf7f1 |
self.connect()
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
if not self.logged_in: # pragma: no cover
|
|
Michal Koutný |
ccf7f1 |
raise BugzillaError("Login with API_KEY failed")
|
|
Michal Koutný |
ccf7f1 |
log.info('API Key accepted')
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
wrote_filename = self._rcfile.save_api_key(self.url, self.api_key)
|
|
Michal Koutný |
ccf7f1 |
log.info("API key written to filename=%s", wrote_filename)
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
msg = "Login successful."
|
|
Michal Koutný |
ccf7f1 |
if wrote_filename:
|
|
Michal Koutný |
ccf7f1 |
msg += " API key written to %s" % wrote_filename
|
|
Michal Koutný |
ccf7f1 |
print(msg)
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
def interactive_login(self, user=None, password=None, force=False,
|
|
Michal Koutný |
ccf7f1 |
restrict_login=None):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
Helper method to handle login for this bugzilla instance.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
:param user: bugzilla username. If not specified, prompt for it.
|
|
Jeff Mahoney |
3dff52 |
:param password: bugzilla password. If not specified, prompt for it.
|
|
Jeff Mahoney |
3dff52 |
:param force: Unused
|
|
Michal Koutný |
ccf7f1 |
:param restrict_login: restricts session to IP address
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
ignore = force
|
|
Jeff Mahoney |
3dff52 |
log.debug('Calling interactive_login')
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
if not user:
|
|
Jeff Mahoney |
3dff52 |
sys.stdout.write('Bugzilla Username: ')
|
|
Jeff Mahoney |
3dff52 |
sys.stdout.flush()
|
|
Jeff Mahoney |
3dff52 |
user = sys.stdin.readline().strip()
|
|
Jeff Mahoney |
3dff52 |
if not password:
|
|
Jeff Mahoney |
3dff52 |
password = getpass.getpass('Bugzilla Password: ')
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
log.info('Logging in... ')
|
|
Michal Koutný |
ccf7f1 |
out = self.login(user, password, restrict_login)
|
|
Michal Koutný |
ccf7f1 |
msg = "Login successful."
|
|
Michal Koutný |
ccf7f1 |
if "token" not in out:
|
|
Michal Koutný |
ccf7f1 |
msg += " However no token was returned."
|
|
Michal Koutný |
ccf7f1 |
else:
|
|
Michal Koutný |
ccf7f1 |
if not self.tokenfile:
|
|
Michal Koutný |
ccf7f1 |
msg += " Token not saved to disk."
|
|
Michal Koutný |
ccf7f1 |
else:
|
|
Michal Koutný |
ccf7f1 |
msg += " Token cache saved to %s" % self.tokenfile
|
|
Michal Koutný |
ccf7f1 |
if self._get_version() >= 5.0:
|
|
Michal Koutný |
ccf7f1 |
msg += "\nToken usage is deprecated. "
|
|
Michal Koutný |
ccf7f1 |
msg += "Consider using bugzilla API keys instead. "
|
|
Michal Koutný |
ccf7f1 |
msg += "See `man bugzilla` for more details."
|
|
Michal Koutný |
ccf7f1 |
print(msg)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def logout(self):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Log out of bugzilla. Drops server connection and user info, and
|
|
Michal Koutný |
ccf7f1 |
destroys authentication cache
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
self._backend.user_logout()
|
|
Jeff Mahoney |
3dff52 |
self.disconnect()
|
|
Jeff Mahoney |
3dff52 |
self.user = ''
|
|
Jeff Mahoney |
3dff52 |
self.password = ''
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
@property
|
|
Jeff Mahoney |
3dff52 |
def logged_in(self):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
This is True if this instance is logged in else False.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
We test if this session is authenticated by calling the User.get()
|
|
Jeff Mahoney |
3dff52 |
XMLRPC method with ids set. Logged-out users cannot pass the 'ids'
|
|
Jeff Mahoney |
3dff52 |
parameter and will result in a 505 error. If we tried to login with a
|
|
Jeff Mahoney |
3dff52 |
token, but the token was incorrect or expired, the server returns a
|
|
Jeff Mahoney |
3dff52 |
32000 error.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
For Bugzilla 5 and later, a new method, User.valid_login is available
|
|
Jeff Mahoney |
3dff52 |
to test the validity of the token. However, this will require that the
|
|
Jeff Mahoney |
3dff52 |
username be cached along with the token in order to work effectively in
|
|
Jeff Mahoney |
3dff52 |
all scenarios and is not currently used. For more information, refer to
|
|
Jeff Mahoney |
3dff52 |
the following url.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
http://bugzilla.readthedocs.org/en/latest/api/core/v1/user.html#valid-login
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
try:
|
|
Michal Koutný |
ccf7f1 |
self._backend.user_get({"ids": [1]})
|
|
Jeff Mahoney |
3dff52 |
return True
|
|
Michal Koutný |
ccf7f1 |
except Exception as e:
|
|
Michal Koutný |
ccf7f1 |
code = BugzillaError.get_bugzilla_error_code(e)
|
|
Michal Koutný |
ccf7f1 |
if code in [505, 32000]:
|
|
Jeff Mahoney |
3dff52 |
return False
|
|
Jeff Mahoney |
3dff52 |
raise e
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
######################
|
|
Jeff Mahoney |
3dff52 |
# Bugfields querying #
|
|
Jeff Mahoney |
3dff52 |
######################
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
def getbugfields(self, force_refresh=False, names=None):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
Calls getBugFields, which returns a list of fields in each bug
|
|
Jeff Mahoney |
3dff52 |
for this bugzilla instance. This can be used to set the list of attrs
|
|
Jeff Mahoney |
3dff52 |
on the Bug object.
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
:param force_refresh: If True, overwrite the bugfield cache
|
|
Michal Koutný |
ccf7f1 |
with these newly checked values.
|
|
Michal Koutný |
ccf7f1 |
:param names: Only check for the passed bug field names
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
def _fieldnames():
|
|
Michal Koutný |
ccf7f1 |
data = {"include_fields": ["name"]}
|
|
Michal Koutný |
ccf7f1 |
if names:
|
|
Michal Koutný |
ccf7f1 |
data["names"] = names
|
|
Michal Koutný |
ccf7f1 |
r = self._backend.bug_fields(data)
|
|
Michal Koutný |
ccf7f1 |
return [f['name'] for f in r['fields']]
|
|
Michal Koutný |
ccf7f1 |
|
|
Jeff Mahoney |
3dff52 |
if force_refresh or not self._cache.bugfields:
|
|
Jeff Mahoney |
3dff52 |
log.debug("Refreshing bugfields")
|
|
Michal Koutný |
ccf7f1 |
self._cache.bugfields = _fieldnames()
|
|
Jeff Mahoney |
3dff52 |
self._cache.bugfields.sort()
|
|
Jeff Mahoney |
3dff52 |
log.debug("bugfields = %s", self._cache.bugfields)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
return self._cache.bugfields
|
|
Jeff Mahoney |
3dff52 |
bugfields = property(fget=lambda self: self.getbugfields(),
|
|
Jeff Mahoney |
3dff52 |
fdel=lambda self: setattr(self, '_bugfields', None))
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
####################
|
|
Jeff Mahoney |
3dff52 |
# Product querying #
|
|
Jeff Mahoney |
3dff52 |
####################
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def product_get(self, ids=None, names=None,
|
|
Jeff Mahoney |
3dff52 |
include_fields=None, exclude_fields=None,
|
|
Jeff Mahoney |
3dff52 |
ptype=None):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
Raw wrapper around Product.get
|
|
Jeff Mahoney |
3dff52 |
https://bugzilla.readthedocs.io/en/latest/api/core/v1/product.html#get-product
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
This does not perform any caching like other product API calls.
|
|
Jeff Mahoney |
3dff52 |
If ids, names, or ptype is not specified, we default to
|
|
Jeff Mahoney |
3dff52 |
ptype=accessible for historical reasons
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
@ids: List of product IDs to lookup
|
|
Jeff Mahoney |
3dff52 |
@names: List of product names to lookup
|
|
Jeff Mahoney |
3dff52 |
@ptype: Either 'accessible', 'selectable', or 'enterable'. If
|
|
Jeff Mahoney |
3dff52 |
specified, we return data for all those
|
|
Jeff Mahoney |
3dff52 |
@include_fields: Only include these fields in the output
|
|
Jeff Mahoney |
3dff52 |
@exclude_fields: Do not include these fields in the output
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
if ids is None and names is None and ptype is None:
|
|
Jeff Mahoney |
3dff52 |
ptype = "accessible"
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
if ptype:
|
|
Jeff Mahoney |
3dff52 |
raw = None
|
|
Jeff Mahoney |
3dff52 |
if ptype == "accessible":
|
|
Michal Koutný |
ccf7f1 |
raw = self._backend.product_get_accessible()
|
|
Jeff Mahoney |
3dff52 |
elif ptype == "enterable":
|
|
Michal Koutný |
ccf7f1 |
raw = self._backend.product_get_enterable()
|
|
Michal Koutný |
ccf7f1 |
elif ptype == "selectable":
|
|
Michal Koutný |
ccf7f1 |
raw = self._backend.product_get_selectable()
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
if raw is None:
|
|
Jeff Mahoney |
3dff52 |
raise RuntimeError("Unknown ptype=%s" % ptype)
|
|
Jeff Mahoney |
3dff52 |
ids = raw['ids']
|
|
Jeff Mahoney |
3dff52 |
log.debug("For ptype=%s found ids=%s", ptype, ids)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
kwargs = {}
|
|
Jeff Mahoney |
3dff52 |
if ids:
|
|
Michal Koutný |
ccf7f1 |
kwargs["ids"] = listify(ids)
|
|
Jeff Mahoney |
3dff52 |
if names:
|
|
Michal Koutný |
ccf7f1 |
kwargs["names"] = listify(names)
|
|
Jeff Mahoney |
3dff52 |
if include_fields:
|
|
Jeff Mahoney |
3dff52 |
kwargs["include_fields"] = include_fields
|
|
Jeff Mahoney |
3dff52 |
if exclude_fields:
|
|
Jeff Mahoney |
3dff52 |
kwargs["exclude_fields"] = exclude_fields
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
ret = self._backend.product_get(kwargs)
|
|
Jeff Mahoney |
3dff52 |
return ret['products']
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def refresh_products(self, **kwargs):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
Refresh a product's cached info. Basically calls product_get
|
|
Jeff Mahoney |
3dff52 |
with the passed arguments, and tries to intelligently update
|
|
Jeff Mahoney |
3dff52 |
our product cache.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
For example, if we already have cached info for product=foo,
|
|
Jeff Mahoney |
3dff52 |
and you pass in names=["bar", "baz"], the new cache will have
|
|
Jeff Mahoney |
3dff52 |
info for products foo, bar, baz. Individual product fields are
|
|
Jeff Mahoney |
3dff52 |
also updated.
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
for product in self.product_get(**kwargs):
|
|
Jeff Mahoney |
3dff52 |
updated = False
|
|
Jeff Mahoney |
3dff52 |
for current in self._cache.products[:]:
|
|
Jeff Mahoney |
3dff52 |
if (current.get("id", -1) != product.get("id", -2) and
|
|
Jeff Mahoney |
3dff52 |
current.get("name", -1) != product.get("name", -2)):
|
|
Jeff Mahoney |
3dff52 |
continue
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
_nested_update(current, product)
|
|
Jeff Mahoney |
3dff52 |
updated = True
|
|
Jeff Mahoney |
3dff52 |
break
|
|
Jeff Mahoney |
3dff52 |
if not updated:
|
|
Jeff Mahoney |
3dff52 |
self._cache.products.append(product)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def getproducts(self, force_refresh=False, **kwargs):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
Query all products and return the raw dict info. Takes all the
|
|
Jeff Mahoney |
3dff52 |
same arguments as product_get.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
On first invocation this will contact bugzilla and internally
|
|
Jeff Mahoney |
3dff52 |
cache the results. Subsequent getproducts calls or accesses to
|
|
Jeff Mahoney |
3dff52 |
self.products will return this cached data only.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
:param force_refresh: force refreshing via refresh_products()
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
if force_refresh or not self._cache.products:
|
|
Jeff Mahoney |
3dff52 |
self.refresh_products(**kwargs)
|
|
Jeff Mahoney |
3dff52 |
return self._cache.products
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
products = property(
|
|
Jeff Mahoney |
3dff52 |
fget=lambda self: self.getproducts(),
|
|
Jeff Mahoney |
3dff52 |
fdel=lambda self: setattr(self, '_products', None),
|
|
Jeff Mahoney |
3dff52 |
doc="Helper for accessing the products cache. If nothing "
|
|
Jeff Mahoney |
3dff52 |
"has been cached yet, this calls getproducts()")
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
#######################
|
|
Jeff Mahoney |
3dff52 |
# components querying #
|
|
Jeff Mahoney |
3dff52 |
#######################
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def _lookup_product_in_cache(self, productname):
|
|
Jeff Mahoney |
3dff52 |
prodstr = isinstance(productname, str) and productname or None
|
|
Jeff Mahoney |
3dff52 |
prodint = isinstance(productname, int) and productname or None
|
|
Jeff Mahoney |
3dff52 |
for proddict in self._cache.products:
|
|
Jeff Mahoney |
3dff52 |
if prodstr == proddict.get("name", -1):
|
|
Jeff Mahoney |
3dff52 |
return proddict
|
|
Jeff Mahoney |
3dff52 |
if prodint == proddict.get("id", "nope"):
|
|
Jeff Mahoney |
3dff52 |
return proddict
|
|
Jeff Mahoney |
3dff52 |
return {}
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def getcomponentsdetails(self, product, force_refresh=False):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
Wrapper around Product.get(include_fields=["components"]),
|
|
Jeff Mahoney |
3dff52 |
returning only the "components" data for the requested product,
|
|
Jeff Mahoney |
3dff52 |
slightly reworked to a dict mapping of components.name: components,
|
|
Jeff Mahoney |
3dff52 |
for historical reasons.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
This uses the product cache, but will update it if the product
|
|
Jeff Mahoney |
3dff52 |
isn't found or "components" isn't cached for the product.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
In cases like bugzilla.redhat.com where there are tons of
|
|
Jeff Mahoney |
3dff52 |
components for some products, this API will time out. You
|
|
Jeff Mahoney |
3dff52 |
should use product_get instead.
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
proddict = self._lookup_product_in_cache(product)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
if (force_refresh or not proddict or "components" not in proddict):
|
|
Jeff Mahoney |
3dff52 |
self.refresh_products(names=[product],
|
|
Jeff Mahoney |
3dff52 |
include_fields=["name", "id", "components"])
|
|
Jeff Mahoney |
3dff52 |
proddict = self._lookup_product_in_cache(product)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
ret = {}
|
|
Jeff Mahoney |
3dff52 |
for compdict in proddict["components"]:
|
|
Jeff Mahoney |
3dff52 |
ret[compdict["name"]] = compdict
|
|
Jeff Mahoney |
3dff52 |
return ret
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def getcomponentdetails(self, product, component, force_refresh=False):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
Helper for accessing a single component's info. This is a wrapper
|
|
Jeff Mahoney |
3dff52 |
around getcomponentsdetails, see that for explanation
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
d = self.getcomponentsdetails(product, force_refresh)
|
|
Jeff Mahoney |
3dff52 |
return d[component]
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def getcomponents(self, product, force_refresh=False):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
Return a list of component names for the passed product.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
On first invocation the value is cached, and subsequent calls
|
|
Jeff Mahoney |
3dff52 |
will return the cached data.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
:param force_refresh: Force refreshing the cache, and return
|
|
Jeff Mahoney |
3dff52 |
the new data
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
proddict = self._lookup_product_in_cache(product)
|
|
Jeff Mahoney |
3dff52 |
product_id = proddict.get("id", None)
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
if (force_refresh or product_id is None or
|
|
Michal Koutný |
ccf7f1 |
"components" not in proddict):
|
|
Michal Koutný |
ccf7f1 |
self.refresh_products(
|
|
Michal Koutný |
ccf7f1 |
names=[product],
|
|
Michal Koutný |
ccf7f1 |
include_fields=["name", "id", "components.name"])
|
|
Jeff Mahoney |
3dff52 |
proddict = self._lookup_product_in_cache(product)
|
|
Michal Koutný |
ccf7f1 |
if "id" not in proddict:
|
|
Michal Koutný |
ccf7f1 |
raise BugzillaError("Product '%s' not found" % product)
|
|
Jeff Mahoney |
3dff52 |
product_id = proddict["id"]
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
if product_id not in self._cache.component_names:
|
|
Michal Koutný |
ccf7f1 |
names = []
|
|
Michal Koutný |
ccf7f1 |
for comp in proddict.get("components", []):
|
|
Michal Koutný |
ccf7f1 |
name = comp.get("name")
|
|
Michal Koutný |
ccf7f1 |
if name:
|
|
Michal Koutný |
ccf7f1 |
names.append(name)
|
|
Jeff Mahoney |
3dff52 |
self._cache.component_names[product_id] = names
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
return self._cache.component_names[product_id]
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
############################
|
|
Jeff Mahoney |
3dff52 |
# component adding/editing #
|
|
Jeff Mahoney |
3dff52 |
############################
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def _component_data_convert(self, data, update=False):
|
|
Jeff Mahoney |
3dff52 |
# Back compat for the old RH interface
|
|
Jeff Mahoney |
3dff52 |
convert_fields = [
|
|
Jeff Mahoney |
3dff52 |
("initialowner", "default_assignee"),
|
|
Jeff Mahoney |
3dff52 |
("initialqacontact", "default_qa_contact"),
|
|
Jeff Mahoney |
3dff52 |
("initialcclist", "default_cc"),
|
|
Jeff Mahoney |
3dff52 |
]
|
|
Jeff Mahoney |
3dff52 |
for old, new in convert_fields:
|
|
Jeff Mahoney |
3dff52 |
if old in data:
|
|
Jeff Mahoney |
3dff52 |
data[new] = data.pop(old)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
if update:
|
|
Jeff Mahoney |
3dff52 |
names = {"product": data.pop("product"),
|
|
Jeff Mahoney |
3dff52 |
"component": data.pop("component")}
|
|
Jeff Mahoney |
3dff52 |
updates = {}
|
|
Jeff Mahoney |
3dff52 |
for k in list(data.keys()):
|
|
Jeff Mahoney |
3dff52 |
updates[k] = data.pop(k)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
data["names"] = [names]
|
|
Jeff Mahoney |
3dff52 |
data["updates"] = updates
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def addcomponent(self, data):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
A method to create a component in Bugzilla. Takes a dict, with the
|
|
Jeff Mahoney |
3dff52 |
following elements:
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
product: The product to create the component in
|
|
Jeff Mahoney |
3dff52 |
component: The name of the component to create
|
|
Michal Koutný |
ccf7f1 |
description: A one sentence summary of the component
|
|
Jeff Mahoney |
3dff52 |
default_assignee: The bugzilla login (email address) of the initial
|
|
Jeff Mahoney |
3dff52 |
owner of the component
|
|
Jeff Mahoney |
3dff52 |
default_qa_contact (optional): The bugzilla login of the
|
|
Jeff Mahoney |
3dff52 |
initial QA contact
|
|
Jeff Mahoney |
3dff52 |
default_cc: (optional) The initial list of users to be CC'ed on
|
|
Jeff Mahoney |
3dff52 |
new bugs for the component.
|
|
Jeff Mahoney |
3dff52 |
is_active: (optional) If False, the component is hidden from
|
|
Jeff Mahoney |
3dff52 |
the component list when filing new bugs.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
data = data.copy()
|
|
Jeff Mahoney |
3dff52 |
self._component_data_convert(data)
|
|
Michal Koutný |
ccf7f1 |
return self._backend.component_create(data)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def editcomponent(self, data):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
A method to edit a component in Bugzilla. Takes a dict, with
|
|
Jeff Mahoney |
3dff52 |
mandatory elements of product. component, and initialowner.
|
|
Jeff Mahoney |
3dff52 |
All other elements are optional and use the same names as the
|
|
Jeff Mahoney |
3dff52 |
addcomponent() method.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
data = data.copy()
|
|
Jeff Mahoney |
3dff52 |
self._component_data_convert(data, update=True)
|
|
Michal Koutný |
ccf7f1 |
return self._backend.component_update(data)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
###################
|
|
Jeff Mahoney |
3dff52 |
# getbug* methods #
|
|
Jeff Mahoney |
3dff52 |
###################
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def _process_include_fields(self, include_fields, exclude_fields,
|
|
Jeff Mahoney |
3dff52 |
extra_fields):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
Internal helper to process include_fields lists
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
def _convert_fields(_in):
|
|
Jeff Mahoney |
3dff52 |
for newname, oldname in self._get_api_aliases():
|
|
Jeff Mahoney |
3dff52 |
if oldname in _in:
|
|
Jeff Mahoney |
3dff52 |
_in.remove(oldname)
|
|
Jeff Mahoney |
3dff52 |
if newname not in _in:
|
|
Jeff Mahoney |
3dff52 |
_in.append(newname)
|
|
Jeff Mahoney |
3dff52 |
return _in
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
ret = {}
|
|
Michal Koutný |
ccf7f1 |
if include_fields:
|
|
Michal Koutný |
ccf7f1 |
include_fields = _convert_fields(include_fields)
|
|
Michal Koutný |
ccf7f1 |
if "id" not in include_fields:
|
|
Michal Koutný |
ccf7f1 |
include_fields.append("id")
|
|
Michal Koutný |
ccf7f1 |
ret["include_fields"] = include_fields
|
|
Michal Koutný |
ccf7f1 |
if exclude_fields:
|
|
Michal Koutný |
ccf7f1 |
exclude_fields = _convert_fields(exclude_fields)
|
|
Michal Koutný |
ccf7f1 |
ret["exclude_fields"] = exclude_fields
|
|
Michal Koutný |
ccf7f1 |
if self._supports_getbug_extra_fields():
|
|
Jeff Mahoney |
3dff52 |
if extra_fields:
|
|
Jeff Mahoney |
3dff52 |
ret["extra_fields"] = _convert_fields(extra_fields)
|
|
Jeff Mahoney |
3dff52 |
return ret
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def _get_bug_autorefresh(self):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
This value is passed to Bug.autorefresh for all fetched bugs.
|
|
Jeff Mahoney |
3dff52 |
If True, and an uncached attribute is requested from a Bug,
|
|
Jeff Mahoney |
3dff52 |
the Bug will update its contents and try again.
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
return self._bug_autorefresh
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def _set_bug_autorefresh(self, val):
|
|
Jeff Mahoney |
3dff52 |
self._bug_autorefresh = bool(val)
|
|
Jeff Mahoney |
3dff52 |
bug_autorefresh = property(_get_bug_autorefresh, _set_bug_autorefresh)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
def _getbug_extra_fields(self):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Extra fields that need to be explicitly
|
|
Michal Koutný |
ccf7f1 |
requested from Bug.get in order for the data to be returned.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
rhbz_extra_fields = [
|
|
Michal Koutný |
ccf7f1 |
"comments", "description",
|
|
Michal Koutný |
ccf7f1 |
"external_bugs", "flags", "sub_components",
|
|
Michal Koutný |
ccf7f1 |
"tags",
|
|
Michal Koutný |
ccf7f1 |
]
|
|
Michal Koutný |
ccf7f1 |
if self._is_redhat_bugzilla:
|
|
Michal Koutný |
ccf7f1 |
return rhbz_extra_fields
|
|
Michal Koutný |
ccf7f1 |
return []
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
def _supports_getbug_extra_fields(self):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Return True if the bugzilla instance supports passing
|
|
Michal Koutný |
ccf7f1 |
extra_fields to getbug
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
As of Dec 2012 it seems like only RH bugzilla actually has behavior
|
|
Michal Koutný |
ccf7f1 |
like this, for upstream bz it returns all info for every Bug.get()
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
return self._is_redhat_bugzilla
|
|
Michal Koutný |
ccf7f1 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def _getbugs(self, idlist, permissive,
|
|
Jeff Mahoney |
3dff52 |
include_fields=None, exclude_fields=None, extra_fields=None):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
Return a list of dicts of full bug info for each given bug id.
|
|
Jeff Mahoney |
3dff52 |
bug ids that couldn't be found will return None instead of a dict.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
ids = []
|
|
Michal Koutný |
ccf7f1 |
aliases = []
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
def _alias_or_int(_v):
|
|
Michal Koutný |
ccf7f1 |
if str(_v).isdigit():
|
|
Michal Koutný |
ccf7f1 |
return int(_v), None
|
|
Michal Koutný |
ccf7f1 |
return None, str(_v)
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
for idstr in idlist:
|
|
Michal Koutný |
ccf7f1 |
idint, alias = _alias_or_int(idstr)
|
|
Michal Koutný |
ccf7f1 |
if alias:
|
|
Michal Koutný |
ccf7f1 |
aliases.append(alias)
|
|
Michal Koutný |
ccf7f1 |
else:
|
|
Michal Koutný |
ccf7f1 |
ids.append(idstr)
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
extra_fields = listify(extra_fields or [])
|
|
Michal Koutný |
ccf7f1 |
extra_fields += self._getbug_extra_fields()
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
getbugdata = {}
|
|
Jeff Mahoney |
3dff52 |
if permissive:
|
|
Jeff Mahoney |
3dff52 |
getbugdata["permissive"] = 1
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
getbugdata.update(self._process_include_fields(
|
|
Jeff Mahoney |
3dff52 |
include_fields, exclude_fields, extra_fields))
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
r = self._backend.bug_get(ids, aliases, getbugdata)
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
# Do some wrangling to ensure we return bugs in the same order
|
|
Michal Koutný |
ccf7f1 |
# the were passed in, for historical reasons
|
|
Jeff Mahoney |
3dff52 |
ret = []
|
|
Michal Koutný |
ccf7f1 |
for idval in idlist:
|
|
Michal Koutný |
ccf7f1 |
idint, alias = _alias_or_int(idval)
|
|
Michal Koutný |
ccf7f1 |
for bugdict in r["bugs"]:
|
|
Michal Koutný |
ccf7f1 |
if idint and idint != bugdict.get("id", None):
|
|
Michal Koutný |
ccf7f1 |
continue
|
|
Michal Koutný |
ccf7f1 |
aliaslist = listify(bugdict.get("alias", None) or [])
|
|
Michal Koutný |
ccf7f1 |
if alias and alias not in aliaslist:
|
|
Michal Koutný |
ccf7f1 |
continue
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
ret.append(bugdict)
|
|
Michal Koutný |
ccf7f1 |
break
|
|
Jeff Mahoney |
3dff52 |
return ret
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def _getbug(self, objid, **kwargs):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
Thin wrapper around _getbugs to handle the slight argument tweaks
|
|
Jeff Mahoney |
3dff52 |
for fetching a single bug. The main bit is permissive=False, which
|
|
Jeff Mahoney |
3dff52 |
will tell bugzilla to raise an explicit error if we can't fetch
|
|
Jeff Mahoney |
3dff52 |
that bug.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
This logic is called from Bug() too
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
return self._getbugs([objid], permissive=False, **kwargs)[0]
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def getbug(self, objid,
|
|
Jeff Mahoney |
3dff52 |
include_fields=None, exclude_fields=None, extra_fields=None):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Return a Bug object with the full complement of bug data
|
|
Michal Koutný |
ccf7f1 |
already loaded.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
data = self._getbug(objid,
|
|
Jeff Mahoney |
3dff52 |
include_fields=include_fields, exclude_fields=exclude_fields,
|
|
Jeff Mahoney |
3dff52 |
extra_fields=extra_fields)
|
|
Jeff Mahoney |
3dff52 |
return Bug(self, dict=data, autorefresh=self.bug_autorefresh)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def getbugs(self, idlist,
|
|
Jeff Mahoney |
3dff52 |
include_fields=None, exclude_fields=None, extra_fields=None,
|
|
Jeff Mahoney |
3dff52 |
permissive=True):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Return a list of Bug objects with the full complement of bug data
|
|
Jeff Mahoney |
3dff52 |
already loaded. If there's a problem getting the data for a given id,
|
|
Michal Koutný |
ccf7f1 |
the corresponding item in the returned list will be None.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
data = self._getbugs(idlist, include_fields=include_fields,
|
|
Jeff Mahoney |
3dff52 |
exclude_fields=exclude_fields, extra_fields=extra_fields,
|
|
Jeff Mahoney |
3dff52 |
permissive=permissive)
|
|
Jeff Mahoney |
3dff52 |
return [(b and Bug(self, dict=b,
|
|
Jeff Mahoney |
3dff52 |
autorefresh=self.bug_autorefresh)) or None
|
|
Jeff Mahoney |
3dff52 |
for b in data]
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def get_comments(self, idlist):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Returns a dictionary of bugs and comments. The comments key will
|
|
Michal Koutný |
ccf7f1 |
be empty. See bugzilla docs for details
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
return self._backend.bug_comments(idlist, {})
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
#################
|
|
Jeff Mahoney |
3dff52 |
# query methods #
|
|
Jeff Mahoney |
3dff52 |
#################
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def build_query(self,
|
|
Jeff Mahoney |
3dff52 |
product=None,
|
|
Jeff Mahoney |
3dff52 |
component=None,
|
|
Jeff Mahoney |
3dff52 |
version=None,
|
|
Jeff Mahoney |
3dff52 |
long_desc=None,
|
|
Jeff Mahoney |
3dff52 |
bug_id=None,
|
|
Jeff Mahoney |
3dff52 |
short_desc=None,
|
|
Jeff Mahoney |
3dff52 |
cc=None,
|
|
Jeff Mahoney |
3dff52 |
assigned_to=None,
|
|
Jeff Mahoney |
3dff52 |
reporter=None,
|
|
Jeff Mahoney |
3dff52 |
qa_contact=None,
|
|
Jeff Mahoney |
3dff52 |
status=None,
|
|
Jeff Mahoney |
3dff52 |
blocked=None,
|
|
Jeff Mahoney |
3dff52 |
dependson=None,
|
|
Jeff Mahoney |
3dff52 |
keywords=None,
|
|
Jeff Mahoney |
3dff52 |
keywords_type=None,
|
|
Jeff Mahoney |
3dff52 |
url=None,
|
|
Jeff Mahoney |
3dff52 |
url_type=None,
|
|
Jeff Mahoney |
3dff52 |
status_whiteboard=None,
|
|
Jeff Mahoney |
3dff52 |
status_whiteboard_type=None,
|
|
Jeff Mahoney |
3dff52 |
fixed_in=None,
|
|
Jeff Mahoney |
3dff52 |
fixed_in_type=None,
|
|
Jeff Mahoney |
3dff52 |
flag=None,
|
|
Jeff Mahoney |
3dff52 |
alias=None,
|
|
Jeff Mahoney |
3dff52 |
qa_whiteboard=None,
|
|
Jeff Mahoney |
3dff52 |
devel_whiteboard=None,
|
|
Jeff Mahoney |
3dff52 |
bug_severity=None,
|
|
Jeff Mahoney |
3dff52 |
priority=None,
|
|
Jeff Mahoney |
3dff52 |
target_release=None,
|
|
Jeff Mahoney |
3dff52 |
target_milestone=None,
|
|
Jeff Mahoney |
3dff52 |
emailtype=None,
|
|
Jeff Mahoney |
3dff52 |
include_fields=None,
|
|
Jeff Mahoney |
3dff52 |
quicksearch=None,
|
|
Jeff Mahoney |
3dff52 |
savedsearch=None,
|
|
Jeff Mahoney |
3dff52 |
savedsearch_sharer_id=None,
|
|
Jeff Mahoney |
3dff52 |
sub_component=None,
|
|
Jeff Mahoney |
3dff52 |
tags=None,
|
|
Jeff Mahoney |
3dff52 |
exclude_fields=None,
|
|
Michal Koutný |
ccf7f1 |
extra_fields=None,
|
|
Michal Koutný |
ccf7f1 |
limit=None):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
Build a query string from passed arguments. Will handle
|
|
Jeff Mahoney |
3dff52 |
query parameter differences between various bugzilla versions.
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
Most of the parameters should be self-explanatory. However,
|
|
Jeff Mahoney |
3dff52 |
if you want to perform a complex query, and easy way is to
|
|
Jeff Mahoney |
3dff52 |
create it with the bugzilla web UI, copy the entire URL it
|
|
Jeff Mahoney |
3dff52 |
generates, and pass it to the static method
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
Bugzilla.url_to_query
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
Then pass the output to Bugzilla.query()
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
For details about the specific argument formats, see the bugzilla docs:
|
|
Jeff Mahoney |
3dff52 |
https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#search-bugs
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
query = {
|
|
Jeff Mahoney |
3dff52 |
"alias": alias,
|
|
Michal Koutný |
ccf7f1 |
"product": listify(product),
|
|
Michal Koutný |
ccf7f1 |
"component": listify(component),
|
|
Jeff Mahoney |
3dff52 |
"version": version,
|
|
Jeff Mahoney |
3dff52 |
"id": bug_id,
|
|
Jeff Mahoney |
3dff52 |
"short_desc": short_desc,
|
|
Jeff Mahoney |
3dff52 |
"bug_status": status,
|
|
Jeff Mahoney |
3dff52 |
"bug_severity": bug_severity,
|
|
Jeff Mahoney |
3dff52 |
"priority": priority,
|
|
Jeff Mahoney |
3dff52 |
"target_release": target_release,
|
|
Jeff Mahoney |
3dff52 |
"target_milestone": target_milestone,
|
|
Michal Koutný |
ccf7f1 |
"tag": listify(tags),
|
|
Jeff Mahoney |
3dff52 |
"quicksearch": quicksearch,
|
|
Jeff Mahoney |
3dff52 |
"savedsearch": savedsearch,
|
|
Jeff Mahoney |
3dff52 |
"sharer_id": savedsearch_sharer_id,
|
|
Michal Koutný |
ccf7f1 |
"limit": limit,
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
# RH extensions... don't add any more. See comment below
|
|
Michal Koutný |
ccf7f1 |
"sub_components": listify(sub_component),
|
|
Jeff Mahoney |
3dff52 |
}
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def add_bool(bzkey, value, bool_id, booltype=None):
|
|
Michal Koutný |
ccf7f1 |
value = listify(value)
|
|
Jeff Mahoney |
3dff52 |
if value is None:
|
|
Jeff Mahoney |
3dff52 |
return bool_id
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
query["query_format"] = "advanced"
|
|
Jeff Mahoney |
3dff52 |
for boolval in value:
|
|
Jeff Mahoney |
3dff52 |
def make_bool_str(prefix):
|
|
Jeff Mahoney |
3dff52 |
# pylint: disable=cell-var-from-loop
|
|
Jeff Mahoney |
3dff52 |
return "%s%i-0-0" % (prefix, bool_id)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
query[make_bool_str("field")] = bzkey
|
|
Jeff Mahoney |
3dff52 |
query[make_bool_str("value")] = boolval
|
|
Jeff Mahoney |
3dff52 |
query[make_bool_str("type")] = booltype or "substring"
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
bool_id += 1
|
|
Jeff Mahoney |
3dff52 |
return bool_id
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
# RH extensions that we have to maintain here for back compat,
|
|
Jeff Mahoney |
3dff52 |
# but all future custom fields should be specified via
|
|
Jeff Mahoney |
3dff52 |
# cli --field option, or via extending the query dict() manually.
|
|
Jeff Mahoney |
3dff52 |
# No more supporting custom fields in this API
|
|
Jeff Mahoney |
3dff52 |
bool_id = 0
|
|
Jeff Mahoney |
3dff52 |
bool_id = add_bool("keywords", keywords, bool_id, keywords_type)
|
|
Jeff Mahoney |
3dff52 |
bool_id = add_bool("blocked", blocked, bool_id)
|
|
Jeff Mahoney |
3dff52 |
bool_id = add_bool("dependson", dependson, bool_id)
|
|
Jeff Mahoney |
3dff52 |
bool_id = add_bool("bug_file_loc", url, bool_id, url_type)
|
|
Jeff Mahoney |
3dff52 |
bool_id = add_bool("cf_fixed_in", fixed_in, bool_id, fixed_in_type)
|
|
Jeff Mahoney |
3dff52 |
bool_id = add_bool("flagtypes.name", flag, bool_id)
|
|
Jeff Mahoney |
3dff52 |
bool_id = add_bool("status_whiteboard",
|
|
Jeff Mahoney |
3dff52 |
status_whiteboard, bool_id, status_whiteboard_type)
|
|
Jeff Mahoney |
3dff52 |
bool_id = add_bool("cf_qa_whiteboard", qa_whiteboard, bool_id)
|
|
Jeff Mahoney |
3dff52 |
bool_id = add_bool("cf_devel_whiteboard", devel_whiteboard, bool_id)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def add_email(key, value, count):
|
|
Jeff Mahoney |
3dff52 |
if value is None:
|
|
Jeff Mahoney |
3dff52 |
return count
|
|
Jeff Mahoney |
3dff52 |
if not emailtype:
|
|
Jeff Mahoney |
3dff52 |
query[key] = value
|
|
Jeff Mahoney |
3dff52 |
return count
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
query["query_format"] = "advanced"
|
|
Jeff Mahoney |
3dff52 |
query['email%i' % count] = value
|
|
Jeff Mahoney |
3dff52 |
query['email%s%i' % (key, count)] = True
|
|
Jeff Mahoney |
3dff52 |
query['emailtype%i' % count] = emailtype
|
|
Jeff Mahoney |
3dff52 |
return count + 1
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
email_count = 1
|
|
Jeff Mahoney |
3dff52 |
email_count = add_email("cc", cc, email_count)
|
|
Jeff Mahoney |
3dff52 |
email_count = add_email("assigned_to", assigned_to, email_count)
|
|
Jeff Mahoney |
3dff52 |
email_count = add_email("reporter", reporter, email_count)
|
|
Jeff Mahoney |
3dff52 |
email_count = add_email("qa_contact", qa_contact, email_count)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
if long_desc is not None:
|
|
Jeff Mahoney |
3dff52 |
query["query_format"] = "advanced"
|
|
Jeff Mahoney |
3dff52 |
query["longdesc"] = long_desc
|
|
Jeff Mahoney |
3dff52 |
query["longdesc_type"] = "allwordssubstr"
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
# 'include_fields' only available for Bugzilla4+
|
|
Jeff Mahoney |
3dff52 |
# 'extra_fields' is an RHBZ extension
|
|
Jeff Mahoney |
3dff52 |
query.update(self._process_include_fields(
|
|
Jeff Mahoney |
3dff52 |
include_fields, exclude_fields, extra_fields))
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
# Strip out None elements in the dict
|
|
Jeff Mahoney |
3dff52 |
for k, v in query.copy().items():
|
|
Jeff Mahoney |
3dff52 |
if v is None:
|
|
Jeff Mahoney |
3dff52 |
del(query[k])
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
self.pre_translation(query)
|
|
Jeff Mahoney |
3dff52 |
return query
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def query(self, query):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Query bugzilla and return a list of matching bugs.
|
|
Jeff Mahoney |
3dff52 |
query must be a dict with fields like those in in querydata['fields'].
|
|
Jeff Mahoney |
3dff52 |
Returns a list of Bug objects.
|
|
Jeff Mahoney |
3dff52 |
Also see the _query() method for details about the underlying
|
|
Jeff Mahoney |
3dff52 |
implementation.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
try:
|
|
Michal Koutný |
ccf7f1 |
r = self._backend.bug_search(query)
|
|
Michal Koutný |
ccf7f1 |
log.debug("bug_search returned:\n%s", str(r))
|
|
Michal Koutný |
ccf7f1 |
except Exception as e:
|
|
Jeff Mahoney |
3dff52 |
# Try to give a hint in the error message if url_to_query
|
|
Jeff Mahoney |
3dff52 |
# isn't supported by this bugzilla instance
|
|
Jeff Mahoney |
3dff52 |
if ("query_format" not in str(e) or
|
|
Michal Koutný |
ccf7f1 |
not BugzillaError.get_bugzilla_error_code(e) or
|
|
Michal Koutný |
ccf7f1 |
self._get_version() >= 5.0):
|
|
Jeff Mahoney |
3dff52 |
raise
|
|
Jeff Mahoney |
3dff52 |
raise BugzillaError("%s\nYour bugzilla instance does not "
|
|
Jeff Mahoney |
3dff52 |
"appear to support API queries derived from bugzilla "
|
|
Michal Koutný |
ccf7f1 |
"web URL queries." % e) from None
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
log.debug("Query returned %s bugs", len(r['bugs']))
|
|
Jeff Mahoney |
3dff52 |
return [Bug(self, dict=b,
|
|
Jeff Mahoney |
3dff52 |
autorefresh=self.bug_autorefresh) for b in r['bugs']]
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def pre_translation(self, query):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
In order to keep the API the same, Bugzilla4 needs to process the
|
|
Jeff Mahoney |
3dff52 |
query and the result. This also applies to the refresh() function
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
if self._is_redhat_bugzilla:
|
|
Michal Koutný |
ccf7f1 |
_RHBugzillaConverters.pre_translation(query)
|
|
Michal Koutný |
ccf7f1 |
query.update(self._process_include_fields(
|
|
Michal Koutný |
ccf7f1 |
query.get("include_fields", []), None, None))
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def post_translation(self, query, bug):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
In order to keep the API the same, Bugzilla4 needs to process the
|
|
Jeff Mahoney |
3dff52 |
query and the result. This also applies to the refresh() function
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
if self._is_redhat_bugzilla:
|
|
Michal Koutný |
ccf7f1 |
_RHBugzillaConverters.post_translation(query, bug)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def bugs_history_raw(self, bug_ids):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
Experimental. Gets the history of changes for
|
|
Jeff Mahoney |
3dff52 |
particular bugs in the database.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
return self._backend.bug_history(bug_ids, {})
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
#######################################
|
|
Jeff Mahoney |
3dff52 |
# Methods for modifying existing bugs #
|
|
Jeff Mahoney |
3dff52 |
#######################################
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
# Bug() also has individual methods for many ops, like setassignee()
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def update_bugs(self, ids, updates):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
A thin wrapper around bugzilla Bug.update(). Used to update all
|
|
Jeff Mahoney |
3dff52 |
values of an existing bug report, as well as add comments.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
The dictionary passed to this function should be generated with
|
|
Jeff Mahoney |
3dff52 |
build_update(), otherwise we cannot guarantee back compatibility.
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
tmp = updates.copy()
|
|
Michal Koutný |
ccf7f1 |
return self._backend.bug_update(listify(ids), tmp)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def update_tags(self, idlist, tags_add=None, tags_remove=None):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
Updates the 'tags' field for a bug.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
tags = {}
|
|
Jeff Mahoney |
3dff52 |
if tags_add:
|
|
Michal Koutný |
ccf7f1 |
tags["add"] = listify(tags_add)
|
|
Jeff Mahoney |
3dff52 |
if tags_remove:
|
|
Michal Koutný |
ccf7f1 |
tags["remove"] = listify(tags_remove)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
d = {
|
|
Jeff Mahoney |
3dff52 |
"tags": tags,
|
|
Jeff Mahoney |
3dff52 |
}
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
return self._backend.bug_update_tags(listify(idlist), d)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def update_flags(self, idlist, flags):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
A thin back compat wrapper around build_update(flags=X)
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
return self.update_bugs(idlist, self.build_update(flags=flags))
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def build_update(self,
|
|
Jeff Mahoney |
3dff52 |
alias=None,
|
|
Jeff Mahoney |
3dff52 |
assigned_to=None,
|
|
Jeff Mahoney |
3dff52 |
blocks_add=None,
|
|
Jeff Mahoney |
3dff52 |
blocks_remove=None,
|
|
Jeff Mahoney |
3dff52 |
blocks_set=None,
|
|
Jeff Mahoney |
3dff52 |
depends_on_add=None,
|
|
Jeff Mahoney |
3dff52 |
depends_on_remove=None,
|
|
Jeff Mahoney |
3dff52 |
depends_on_set=None,
|
|
Jeff Mahoney |
3dff52 |
cc_add=None,
|
|
Jeff Mahoney |
3dff52 |
cc_remove=None,
|
|
Jeff Mahoney |
3dff52 |
is_cc_accessible=None,
|
|
Jeff Mahoney |
3dff52 |
comment=None,
|
|
Jeff Mahoney |
3dff52 |
comment_private=None,
|
|
Jeff Mahoney |
3dff52 |
component=None,
|
|
Jeff Mahoney |
3dff52 |
deadline=None,
|
|
Jeff Mahoney |
3dff52 |
dupe_of=None,
|
|
Jeff Mahoney |
3dff52 |
estimated_time=None,
|
|
Jeff Mahoney |
3dff52 |
groups_add=None,
|
|
Jeff Mahoney |
3dff52 |
groups_remove=None,
|
|
Jeff Mahoney |
3dff52 |
keywords_add=None,
|
|
Jeff Mahoney |
3dff52 |
keywords_remove=None,
|
|
Jeff Mahoney |
3dff52 |
keywords_set=None,
|
|
Jeff Mahoney |
3dff52 |
op_sys=None,
|
|
Jeff Mahoney |
3dff52 |
platform=None,
|
|
Jeff Mahoney |
3dff52 |
priority=None,
|
|
Jeff Mahoney |
3dff52 |
product=None,
|
|
Jeff Mahoney |
3dff52 |
qa_contact=None,
|
|
Jeff Mahoney |
3dff52 |
is_creator_accessible=None,
|
|
Jeff Mahoney |
3dff52 |
remaining_time=None,
|
|
Jeff Mahoney |
3dff52 |
reset_assigned_to=None,
|
|
Jeff Mahoney |
3dff52 |
reset_qa_contact=None,
|
|
Jeff Mahoney |
3dff52 |
resolution=None,
|
|
Jeff Mahoney |
3dff52 |
see_also_add=None,
|
|
Jeff Mahoney |
3dff52 |
see_also_remove=None,
|
|
Jeff Mahoney |
3dff52 |
severity=None,
|
|
Jeff Mahoney |
3dff52 |
status=None,
|
|
Jeff Mahoney |
3dff52 |
summary=None,
|
|
Jeff Mahoney |
3dff52 |
target_milestone=None,
|
|
Jeff Mahoney |
3dff52 |
target_release=None,
|
|
Jeff Mahoney |
3dff52 |
url=None,
|
|
Jeff Mahoney |
3dff52 |
version=None,
|
|
Jeff Mahoney |
3dff52 |
whiteboard=None,
|
|
Jeff Mahoney |
3dff52 |
work_time=None,
|
|
Jeff Mahoney |
3dff52 |
fixed_in=None,
|
|
Jeff Mahoney |
3dff52 |
qa_whiteboard=None,
|
|
Jeff Mahoney |
3dff52 |
devel_whiteboard=None,
|
|
Jeff Mahoney |
3dff52 |
internal_whiteboard=None,
|
|
Jeff Mahoney |
3dff52 |
sub_component=None,
|
|
Jeff Mahoney |
3dff52 |
flags=None,
|
|
Michal Koutný |
ccf7f1 |
comment_tags=None,
|
|
Michal Koutný |
ccf7f1 |
minor_update=None):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
Returns a python dict() with properly formatted parameters to
|
|
Jeff Mahoney |
3dff52 |
pass to update_bugs(). See bugzilla documentation for the format
|
|
Jeff Mahoney |
3dff52 |
of the individual fields:
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#create-bug
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
ret = {}
|
|
Michal Koutný |
ccf7f1 |
rhbzret = {}
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
# These are only supported for rhbugzilla
|
|
Michal Koutný |
ccf7f1 |
#
|
|
Michal Koutný |
ccf7f1 |
# This should not be extended any more.
|
|
Michal Koutný |
ccf7f1 |
# If people want to handle custom fields, manually extend the
|
|
Michal Koutný |
ccf7f1 |
# returned dictionary.
|
|
Michal Koutný |
ccf7f1 |
rhbzargs = {
|
|
Michal Koutný |
ccf7f1 |
"fixed_in": fixed_in,
|
|
Michal Koutný |
ccf7f1 |
"devel_whiteboard": devel_whiteboard,
|
|
Michal Koutný |
ccf7f1 |
"qa_whiteboard": qa_whiteboard,
|
|
Michal Koutný |
ccf7f1 |
"internal_whiteboard": internal_whiteboard,
|
|
Michal Koutný |
ccf7f1 |
"sub_component": sub_component,
|
|
Michal Koutný |
ccf7f1 |
}
|
|
Michal Koutný |
ccf7f1 |
if self._is_redhat_bugzilla:
|
|
Michal Koutný |
ccf7f1 |
rhbzret = _RHBugzillaConverters.convert_build_update(
|
|
Michal Koutný |
ccf7f1 |
component=component, **rhbzargs)
|
|
Michal Koutný |
ccf7f1 |
else:
|
|
Michal Koutný |
ccf7f1 |
for key, val in rhbzargs.items():
|
|
Michal Koutný |
ccf7f1 |
if val is not None:
|
|
Michal Koutný |
ccf7f1 |
raise ValueError("bugzilla instance does not support "
|
|
Michal Koutný |
ccf7f1 |
"updating '%s'" % key)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def s(key, val, convert=None):
|
|
Jeff Mahoney |
3dff52 |
if val is None:
|
|
Jeff Mahoney |
3dff52 |
return
|
|
Jeff Mahoney |
3dff52 |
if convert:
|
|
Jeff Mahoney |
3dff52 |
val = convert(val)
|
|
Jeff Mahoney |
3dff52 |
ret[key] = val
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def add_dict(key, add, remove, _set=None, convert=None):
|
|
Jeff Mahoney |
3dff52 |
if add is remove is _set is None:
|
|
Jeff Mahoney |
3dff52 |
return
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def c(val):
|
|
Michal Koutný |
ccf7f1 |
val = listify(val)
|
|
Jeff Mahoney |
3dff52 |
if convert:
|
|
Jeff Mahoney |
3dff52 |
val = [convert(v) for v in val]
|
|
Jeff Mahoney |
3dff52 |
return val
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
newdict = {}
|
|
Jeff Mahoney |
3dff52 |
if add is not None:
|
|
Jeff Mahoney |
3dff52 |
newdict["add"] = c(add)
|
|
Jeff Mahoney |
3dff52 |
if remove is not None:
|
|
Jeff Mahoney |
3dff52 |
newdict["remove"] = c(remove)
|
|
Jeff Mahoney |
3dff52 |
if _set is not None:
|
|
Jeff Mahoney |
3dff52 |
newdict["set"] = c(_set)
|
|
Jeff Mahoney |
3dff52 |
ret[key] = newdict
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
s("alias", alias)
|
|
Jeff Mahoney |
3dff52 |
s("assigned_to", assigned_to)
|
|
Jeff Mahoney |
3dff52 |
s("is_cc_accessible", is_cc_accessible, bool)
|
|
Jeff Mahoney |
3dff52 |
s("component", component)
|
|
Jeff Mahoney |
3dff52 |
s("deadline", deadline)
|
|
Jeff Mahoney |
3dff52 |
s("dupe_of", dupe_of, int)
|
|
Jeff Mahoney |
3dff52 |
s("estimated_time", estimated_time, int)
|
|
Jeff Mahoney |
3dff52 |
s("op_sys", op_sys)
|
|
Jeff Mahoney |
3dff52 |
s("platform", platform)
|
|
Jeff Mahoney |
3dff52 |
s("priority", priority)
|
|
Jeff Mahoney |
3dff52 |
s("product", product)
|
|
Jeff Mahoney |
3dff52 |
s("qa_contact", qa_contact)
|
|
Jeff Mahoney |
3dff52 |
s("is_creator_accessible", is_creator_accessible, bool)
|
|
Jeff Mahoney |
3dff52 |
s("remaining_time", remaining_time, float)
|
|
Jeff Mahoney |
3dff52 |
s("reset_assigned_to", reset_assigned_to, bool)
|
|
Jeff Mahoney |
3dff52 |
s("reset_qa_contact", reset_qa_contact, bool)
|
|
Jeff Mahoney |
3dff52 |
s("resolution", resolution)
|
|
Jeff Mahoney |
3dff52 |
s("severity", severity)
|
|
Jeff Mahoney |
3dff52 |
s("status", status)
|
|
Jeff Mahoney |
3dff52 |
s("summary", summary)
|
|
Jeff Mahoney |
3dff52 |
s("target_milestone", target_milestone)
|
|
Jeff Mahoney |
3dff52 |
s("target_release", target_release)
|
|
Jeff Mahoney |
3dff52 |
s("url", url)
|
|
Jeff Mahoney |
3dff52 |
s("version", version)
|
|
Jeff Mahoney |
3dff52 |
s("whiteboard", whiteboard)
|
|
Jeff Mahoney |
3dff52 |
s("work_time", work_time, float)
|
|
Jeff Mahoney |
3dff52 |
s("flags", flags)
|
|
Michal Koutný |
ccf7f1 |
s("comment_tags", comment_tags, listify)
|
|
Michal Koutný |
ccf7f1 |
s("minor_update", minor_update, bool)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
add_dict("blocks", blocks_add, blocks_remove, blocks_set,
|
|
Jeff Mahoney |
3dff52 |
convert=int)
|
|
Jeff Mahoney |
3dff52 |
add_dict("depends_on", depends_on_add, depends_on_remove,
|
|
Jeff Mahoney |
3dff52 |
depends_on_set, convert=int)
|
|
Jeff Mahoney |
3dff52 |
add_dict("cc", cc_add, cc_remove)
|
|
Jeff Mahoney |
3dff52 |
add_dict("groups", groups_add, groups_remove)
|
|
Jeff Mahoney |
3dff52 |
add_dict("keywords", keywords_add, keywords_remove, keywords_set)
|
|
Jeff Mahoney |
3dff52 |
add_dict("see_also", see_also_add, see_also_remove)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
if comment is not None:
|
|
Jeff Mahoney |
3dff52 |
ret["comment"] = {"comment": comment}
|
|
Jeff Mahoney |
3dff52 |
if comment_private:
|
|
Jeff Mahoney |
3dff52 |
ret["comment"]["is_private"] = comment_private
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
ret.update(rhbzret)
|
|
Jeff Mahoney |
3dff52 |
return ret
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
########################################
|
|
Jeff Mahoney |
3dff52 |
# Methods for working with attachments #
|
|
Jeff Mahoney |
3dff52 |
########################################
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def attachfile(self, idlist, attachfile, description, **kwargs):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
Attach a file to the given bug IDs. Returns the ID of the attachment
|
|
Jeff Mahoney |
3dff52 |
or raises XMLRPC Fault if something goes wrong.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
attachfile may be a filename (which will be opened) or a file-like
|
|
Jeff Mahoney |
3dff52 |
object, which must provide a 'read' method. If it's not one of these,
|
|
Jeff Mahoney |
3dff52 |
this method will raise a TypeError.
|
|
Jeff Mahoney |
3dff52 |
description is the short description of this attachment.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
Optional keyword args are as follows:
|
|
Jeff Mahoney |
3dff52 |
file_name: this will be used as the filename for the attachment.
|
|
Jeff Mahoney |
3dff52 |
REQUIRED if attachfile is a file-like object with no
|
|
Jeff Mahoney |
3dff52 |
'name' attribute, otherwise the filename or .name
|
|
Jeff Mahoney |
3dff52 |
attribute will be used.
|
|
Jeff Mahoney |
3dff52 |
comment: An optional comment about this attachment.
|
|
Jeff Mahoney |
3dff52 |
is_private: Set to True if the attachment should be marked private.
|
|
Jeff Mahoney |
3dff52 |
is_patch: Set to True if the attachment is a patch.
|
|
Jeff Mahoney |
3dff52 |
content_type: The mime-type of the attached file. Defaults to
|
|
Jeff Mahoney |
3dff52 |
application/octet-stream if not set. NOTE that text
|
|
Jeff Mahoney |
3dff52 |
files will *not* be viewable in bugzilla unless you
|
|
Jeff Mahoney |
3dff52 |
remember to set this to text/plain. So remember that!
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
Returns the list of attachment ids that were added. If only one
|
|
Jeff Mahoney |
3dff52 |
attachment was added, we return the single int ID for back compat
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
if isinstance(attachfile, str):
|
|
Jeff Mahoney |
3dff52 |
f = open(attachfile, "rb")
|
|
Jeff Mahoney |
3dff52 |
elif hasattr(attachfile, 'read'):
|
|
Jeff Mahoney |
3dff52 |
f = attachfile
|
|
Jeff Mahoney |
3dff52 |
else:
|
|
Jeff Mahoney |
3dff52 |
raise TypeError("attachfile must be filename or file-like object")
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
# Back compat
|
|
Jeff Mahoney |
3dff52 |
if "contenttype" in kwargs:
|
|
Jeff Mahoney |
3dff52 |
kwargs["content_type"] = kwargs.pop("contenttype")
|
|
Jeff Mahoney |
3dff52 |
if "ispatch" in kwargs:
|
|
Jeff Mahoney |
3dff52 |
kwargs["is_patch"] = kwargs.pop("ispatch")
|
|
Jeff Mahoney |
3dff52 |
if "isprivate" in kwargs:
|
|
Jeff Mahoney |
3dff52 |
kwargs["is_private"] = kwargs.pop("isprivate")
|
|
Jeff Mahoney |
3dff52 |
if "filename" in kwargs:
|
|
Jeff Mahoney |
3dff52 |
kwargs["file_name"] = kwargs.pop("filename")
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
kwargs['summary'] = description
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
data = f.read()
|
|
Michal Koutný |
ccf7f1 |
if not isinstance(data, bytes): # pragma: no cover
|
|
Jeff Mahoney |
3dff52 |
data = data.encode(locale.getpreferredencoding())
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
if 'file_name' not in kwargs and hasattr(f, "name"):
|
|
Jeff Mahoney |
3dff52 |
kwargs['file_name'] = os.path.basename(f.name)
|
|
Jeff Mahoney |
3dff52 |
if 'content_type' not in kwargs:
|
|
Michal Koutný |
ccf7f1 |
ctype = None
|
|
Michal Koutný |
ccf7f1 |
if kwargs['file_name']:
|
|
Michal Koutný |
ccf7f1 |
ctype = mimetypes.guess_type(
|
|
Michal Koutný |
ccf7f1 |
kwargs['file_name'], strict=False)[0]
|
|
Michal Koutný |
ccf7f1 |
kwargs['content_type'] = ctype or 'application/octet-stream'
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
ret = self._backend.bug_attachment_create(
|
|
Michal Koutný |
ccf7f1 |
listify(idlist), data, kwargs)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
if "attachments" in ret:
|
|
Jeff Mahoney |
3dff52 |
# Up to BZ 4.2
|
|
Jeff Mahoney |
3dff52 |
ret = [int(k) for k in ret["attachments"].keys()]
|
|
Jeff Mahoney |
3dff52 |
elif "ids" in ret:
|
|
Jeff Mahoney |
3dff52 |
# BZ 4.4+
|
|
Jeff Mahoney |
3dff52 |
ret = ret["ids"]
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
if isinstance(ret, list) and len(ret) == 1:
|
|
Jeff Mahoney |
3dff52 |
ret = ret[0]
|
|
Jeff Mahoney |
3dff52 |
return ret
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
def openattachment_data(self, attachment_dict):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Helper for turning passed API attachment dictionary into a
|
|
Michal Koutný |
ccf7f1 |
filelike object
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
ret = BytesIO()
|
|
Michal Koutný |
ccf7f1 |
data = attachment_dict["data"]
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
if hasattr(data, "data"):
|
|
Michal Koutný |
ccf7f1 |
# This is for xmlrpc Binary
|
|
Michal Koutný |
ccf7f1 |
content = data.data # pragma: no cover
|
|
Michal Koutný |
ccf7f1 |
else:
|
|
Michal Koutný |
ccf7f1 |
import base64
|
|
Michal Koutný |
ccf7f1 |
content = base64.b64decode(data)
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
ret.write(content)
|
|
Michal Koutný |
ccf7f1 |
ret.name = attachment_dict["file_name"]
|
|
Jeff Mahoney |
3dff52 |
ret.seek(0)
|
|
Jeff Mahoney |
3dff52 |
return ret
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
def openattachment(self, attachid):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Get the contents of the attachment with the given attachment ID.
|
|
Michal Koutný |
ccf7f1 |
Returns a file-like object.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
attachments = self.get_attachments(None, attachid)
|
|
Michal Koutný |
ccf7f1 |
data = attachments["attachments"][str(attachid)]
|
|
Michal Koutný |
ccf7f1 |
return self.openattachment_data(data)
|
|
Michal Koutný |
ccf7f1 |
|
|
Jeff Mahoney |
3dff52 |
def updateattachmentflags(self, bugid, attachid, flagname, **kwargs):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
Updates a flag for the given attachment ID.
|
|
Jeff Mahoney |
3dff52 |
Optional keyword args are:
|
|
Jeff Mahoney |
3dff52 |
status: new status for the flag ('-', '+', '?', 'X')
|
|
Jeff Mahoney |
3dff52 |
requestee: new requestee for the flag
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
# Bug ID was used for the original custom redhat API, no longer
|
|
Jeff Mahoney |
3dff52 |
# needed though
|
|
Jeff Mahoney |
3dff52 |
ignore = bugid
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
flags = {"name": flagname}
|
|
Jeff Mahoney |
3dff52 |
flags.update(kwargs)
|
|
Michal Koutný |
ccf7f1 |
attachment_ids = [int(attachid)]
|
|
Michal Koutný |
ccf7f1 |
update = {'flags': [flags]}
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
return self._backend.bug_attachment_update(attachment_ids, update)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def get_attachments(self, ids, attachment_ids,
|
|
Jeff Mahoney |
3dff52 |
include_fields=None, exclude_fields=None):
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
Wrapper for Bug.attachments. One of ids or attachment_ids is required
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
:param ids: Get attachments for this bug ID
|
|
Jeff Mahoney |
3dff52 |
:param attachment_ids: Specific attachment ID to get
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
https://bugzilla.readthedocs.io/en/latest/api/core/v1/attachment.html#get-attachment
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Michal Koutný |
ccf7f1 |
params = {}
|
|
Jeff Mahoney |
3dff52 |
if include_fields:
|
|
Michal Koutný |
ccf7f1 |
params["include_fields"] = listify(include_fields)
|
|
Jeff Mahoney |
3dff52 |
if exclude_fields:
|
|
Michal Koutný |
ccf7f1 |
params["exclude_fields"] = listify(exclude_fields)
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
if attachment_ids:
|
|
Michal Koutný |
ccf7f1 |
return self._backend.bug_attachment_get(attachment_ids, params)
|
|
Michal Koutný |
ccf7f1 |
return self._backend.bug_attachment_get_all(ids, params)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
#####################
|
|
Jeff Mahoney |
3dff52 |
# createbug methods #
|
|
Jeff Mahoney |
3dff52 |
#####################
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
createbug_required = ('product', 'component', 'summary', 'version',
|
|
Jeff Mahoney |
3dff52 |
'description')
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def build_createbug(self,
|
|
Jeff Mahoney |
3dff52 |
product=None,
|
|
Jeff Mahoney |
3dff52 |
component=None,
|
|
Jeff Mahoney |
3dff52 |
version=None,
|
|
Jeff Mahoney |
3dff52 |
summary=None,
|
|
Jeff Mahoney |
3dff52 |
description=None,
|
|
Jeff Mahoney |
3dff52 |
comment_private=None,
|
|
Jeff Mahoney |
3dff52 |
blocks=None,
|
|
Jeff Mahoney |
3dff52 |
cc=None,
|
|
Jeff Mahoney |
3dff52 |
assigned_to=None,
|
|
Jeff Mahoney |
3dff52 |
keywords=None,
|
|
Jeff Mahoney |
3dff52 |
depends_on=None,
|
|
Jeff Mahoney |
3dff52 |
groups=None,
|
|
Jeff Mahoney |
3dff52 |
op_sys=None,
|
|
Jeff Mahoney |
3dff52 |
platform=None,
|
|
Jeff Mahoney |
3dff52 |
priority=None,
|
|
Jeff Mahoney |
3dff52 |
qa_contact=None,
|
|
Jeff Mahoney |
3dff52 |
resolution=None,
|
|
Jeff Mahoney |
3dff52 |
severity=None,
|
|
Jeff Mahoney |
3dff52 |
status=None,
|
|
Jeff Mahoney |
3dff52 |
target_milestone=None,
|
|
Jeff Mahoney |
3dff52 |
target_release=None,
|
|
Jeff Mahoney |
3dff52 |
url=None,
|
|
Jeff Mahoney |
3dff52 |
sub_component=None,
|
|
Jeff Mahoney |
3dff52 |
alias=None,
|
|
Jeff Mahoney |
3dff52 |
comment_tags=None):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
Returns a python dict() with properly formatted parameters to
|
|
Jeff Mahoney |
3dff52 |
pass to createbug(). See bugzilla documentation for the format
|
|
Jeff Mahoney |
3dff52 |
of the individual fields:
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#update-bug
|
|
Jeff Mahoney |
3dff52 |
"""
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
localdict = {}
|
|
Jeff Mahoney |
3dff52 |
if blocks:
|
|
Michal Koutný |
ccf7f1 |
localdict["blocks"] = listify(blocks)
|
|
Jeff Mahoney |
3dff52 |
if cc:
|
|
Michal Koutný |
ccf7f1 |
localdict["cc"] = listify(cc)
|
|
Jeff Mahoney |
3dff52 |
if depends_on:
|
|
Michal Koutný |
ccf7f1 |
localdict["depends_on"] = listify(depends_on)
|
|
Jeff Mahoney |
3dff52 |
if groups:
|
|
Michal Koutný |
ccf7f1 |
localdict["groups"] = listify(groups)
|
|
Jeff Mahoney |
3dff52 |
if keywords:
|
|
Michal Koutný |
ccf7f1 |
localdict["keywords"] = listify(keywords)
|
|
Jeff Mahoney |
3dff52 |
if description:
|
|
Jeff Mahoney |
3dff52 |
localdict["description"] = description
|
|
Jeff Mahoney |
3dff52 |
if comment_private:
|
|
Jeff Mahoney |
3dff52 |
localdict["comment_is_private"] = True
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
# Most of the machinery and formatting here is the same as
|
|
Jeff Mahoney |
3dff52 |
# build_update, so reuse that as much as possible
|
|
Jeff Mahoney |
3dff52 |
ret = self.build_update(product=product, component=component,
|
|
Jeff Mahoney |
3dff52 |
version=version, summary=summary, op_sys=op_sys,
|
|
Jeff Mahoney |
3dff52 |
platform=platform, priority=priority, qa_contact=qa_contact,
|
|
Jeff Mahoney |
3dff52 |
resolution=resolution, severity=severity, status=status,
|
|
Jeff Mahoney |
3dff52 |
target_milestone=target_milestone,
|
|
Jeff Mahoney |
3dff52 |
target_release=target_release, url=url,
|
|
Jeff Mahoney |
3dff52 |
assigned_to=assigned_to, sub_component=sub_component,
|
|
Jeff Mahoney |
3dff52 |
alias=alias, comment_tags=comment_tags)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
ret.update(localdict)
|
|
Jeff Mahoney |
3dff52 |
return ret
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def _validate_createbug(self, *args, **kwargs):
|
|
Jeff Mahoney |
3dff52 |
# Previous API required users specifying keyword args that mapped
|
|
Jeff Mahoney |
3dff52 |
# to the XMLRPC arg names. Maintain that bad compat, but also allow
|
|
Jeff Mahoney |
3dff52 |
# receiving a single dictionary like query() does
|
|
Michal Koutný |
ccf7f1 |
if kwargs and args: # pragma: no cover
|
|
Jeff Mahoney |
3dff52 |
raise BugzillaError("createbug: cannot specify positional "
|
|
Jeff Mahoney |
3dff52 |
"args=%s with kwargs=%s, must be one or the "
|
|
Jeff Mahoney |
3dff52 |
"other." % (args, kwargs))
|
|
Jeff Mahoney |
3dff52 |
if args:
|
|
Jeff Mahoney |
3dff52 |
if len(args) > 1 or not isinstance(args[0], dict):
|
|
Michal Koutný |
ccf7f1 |
raise BugzillaError( # pragma: no cover
|
|
Michal Koutný |
ccf7f1 |
"createbug: positional arguments only "
|
|
Michal Koutný |
ccf7f1 |
"accept a single dictionary.")
|
|
Jeff Mahoney |
3dff52 |
data = args[0]
|
|
Jeff Mahoney |
3dff52 |
else:
|
|
Jeff Mahoney |
3dff52 |
data = kwargs
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
# If we're getting a call that uses an old fieldname, convert it to the
|
|
Jeff Mahoney |
3dff52 |
# new fieldname instead.
|
|
Jeff Mahoney |
3dff52 |
for newname, oldname in self._get_api_aliases():
|
|
Jeff Mahoney |
3dff52 |
if (newname in self.createbug_required and
|
|
Jeff Mahoney |
3dff52 |
newname not in data and
|
|
Jeff Mahoney |
3dff52 |
oldname in data):
|
|
Jeff Mahoney |
3dff52 |
data[newname] = data.pop(oldname)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
# Back compat handling for check_args
|
|
Jeff Mahoney |
3dff52 |
if "check_args" in data:
|
|
Jeff Mahoney |
3dff52 |
del(data["check_args"])
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
return data
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def createbug(self, *args, **kwargs):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
Create a bug with the given info. Returns a new Bug object.
|
|
Jeff Mahoney |
3dff52 |
Check bugzilla API documentation for valid values, at least
|
|
Jeff Mahoney |
3dff52 |
product, component, summary, version, and description need to
|
|
Jeff Mahoney |
3dff52 |
be passed.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
data = self._validate_createbug(*args, **kwargs)
|
|
Michal Koutný |
ccf7f1 |
rawbug = self._backend.bug_create(data)
|
|
Jeff Mahoney |
3dff52 |
return Bug(self, bug_id=rawbug["id"],
|
|
Jeff Mahoney |
3dff52 |
autorefresh=self.bug_autorefresh)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
##############################
|
|
Jeff Mahoney |
3dff52 |
# Methods for handling Users #
|
|
Jeff Mahoney |
3dff52 |
##############################
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def getuser(self, username):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Return a bugzilla User for the given username
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
:arg username: The username used in bugzilla.
|
|
Jeff Mahoney |
3dff52 |
:raises XMLRPC Fault: Code 51 if the username does not exist
|
|
Jeff Mahoney |
3dff52 |
:returns: User record for the username
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
ret = self.getusers(username)
|
|
Jeff Mahoney |
3dff52 |
return ret and ret[0]
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def getusers(self, userlist):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Return a list of Users from .
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
:userlist: List of usernames to lookup
|
|
Jeff Mahoney |
3dff52 |
:returns: List of User records
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
userlist = listify(userlist)
|
|
Michal Koutný |
ccf7f1 |
rawusers = self._backend.user_get({"names": userlist})
|
|
Jeff Mahoney |
3dff52 |
userobjs = [User(self, **rawuser) for rawuser in
|
|
Michal Koutný |
ccf7f1 |
rawusers.get('users', [])]
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
# Return users in same order they were passed in
|
|
Jeff Mahoney |
3dff52 |
ret = []
|
|
Jeff Mahoney |
3dff52 |
for u in userlist:
|
|
Jeff Mahoney |
3dff52 |
for uobj in userobjs[:]:
|
|
Jeff Mahoney |
3dff52 |
if uobj.email == u:
|
|
Jeff Mahoney |
3dff52 |
userobjs.remove(uobj)
|
|
Jeff Mahoney |
3dff52 |
ret.append(uobj)
|
|
Jeff Mahoney |
3dff52 |
break
|
|
Jeff Mahoney |
3dff52 |
ret += userobjs
|
|
Jeff Mahoney |
3dff52 |
return ret
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def searchusers(self, pattern):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Return a bugzilla User for the given list of patterns
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
:arg pattern: List of patterns to match against.
|
|
Jeff Mahoney |
3dff52 |
:returns: List of User records
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
rawusers = self._backend.user_get({"match": listify(pattern)})
|
|
Jeff Mahoney |
3dff52 |
return [User(self, **rawuser) for rawuser in
|
|
Michal Koutný |
ccf7f1 |
rawusers.get('users', [])]
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def createuser(self, email, name='', password=''):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Return a bugzilla User for the given username
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
:arg email: The email address to use in bugzilla
|
|
Jeff Mahoney |
3dff52 |
:kwarg name: Real name to associate with the account
|
|
Jeff Mahoney |
3dff52 |
:kwarg password: Password to set for the bugzilla account
|
|
Jeff Mahoney |
3dff52 |
:raises XMLRPC Fault: Code 501 if the username already exists
|
|
Jeff Mahoney |
3dff52 |
Code 500 if the email address isn't valid
|
|
Jeff Mahoney |
3dff52 |
Code 502 if the password is too short
|
|
Jeff Mahoney |
3dff52 |
Code 503 if the password is too long
|
|
Jeff Mahoney |
3dff52 |
:return: User record for the username
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
args = {"email": email}
|
|
Michal Koutný |
ccf7f1 |
if name:
|
|
Michal Koutný |
ccf7f1 |
args["name"] = name
|
|
Michal Koutný |
ccf7f1 |
if password:
|
|
Michal Koutný |
ccf7f1 |
args["password"] = password
|
|
Michal Koutný |
ccf7f1 |
self._backend.user_create(args)
|
|
Jeff Mahoney |
3dff52 |
return self.getuser(email)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
def updateperms(self, user, action, groups):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Jeff Mahoney |
3dff52 |
A method to update the permissions (group membership) of a bugzilla
|
|
Jeff Mahoney |
3dff52 |
user.
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
:arg user: The e-mail address of the user to be acted upon. Can
|
|
Jeff Mahoney |
3dff52 |
also be a list of emails.
|
|
Jeff Mahoney |
3dff52 |
:arg action: add, remove, or set
|
|
Jeff Mahoney |
3dff52 |
:arg groups: list of groups to be added to (i.e. ['fedora_contrib'])
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
groups = listify(groups)
|
|
Jeff Mahoney |
3dff52 |
if action == "rem":
|
|
Jeff Mahoney |
3dff52 |
action = "remove"
|
|
Jeff Mahoney |
3dff52 |
if action not in ["add", "remove", "set"]:
|
|
Jeff Mahoney |
3dff52 |
raise BugzillaError("Unknown user permission action '%s'" % action)
|
|
Jeff Mahoney |
3dff52 |
|
|
Jeff Mahoney |
3dff52 |
update = {
|
|
Michal Koutný |
ccf7f1 |
"names": listify(user),
|
|
Jeff Mahoney |
3dff52 |
"groups": {
|
|
Jeff Mahoney |
3dff52 |
action: groups,
|
|
Jeff Mahoney |
3dff52 |
}
|
|
Jeff Mahoney |
3dff52 |
}
|
|
Jeff Mahoney |
3dff52 |
|
|
Michal Koutný |
ccf7f1 |
return self._backend.user_update(update)
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
###############################
|
|
Michal Koutný |
ccf7f1 |
# Methods for handling Groups #
|
|
Michal Koutný |
ccf7f1 |
###############################
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
def _getgroups(self, names, membership=False):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Return a list of groups that match criteria.
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
:kwarg ids: list of group ids to return data on
|
|
Michal Koutný |
ccf7f1 |
:kwarg membership: boolean specifying wether to query the members
|
|
Michal Koutný |
ccf7f1 |
of the group or not.
|
|
Michal Koutný |
ccf7f1 |
:raises XMLRPC Fault: Code 51: if a Bad Login Name was sent to the
|
|
Michal Koutný |
ccf7f1 |
names array.
|
|
Michal Koutný |
ccf7f1 |
Code 304: if the user was not authorized to see user they
|
|
Michal Koutný |
ccf7f1 |
requested.
|
|
Michal Koutný |
ccf7f1 |
Code 505: user is logged out and can't use the match or ids
|
|
Michal Koutný |
ccf7f1 |
parameter.
|
|
Michal Koutný |
ccf7f1 |
Code 805: logged in user do not have enough priviledges to view
|
|
Michal Koutný |
ccf7f1 |
groups.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
params = {"membership": membership}
|
|
Michal Koutný |
ccf7f1 |
params['names'] = listify(names)
|
|
Michal Koutný |
ccf7f1 |
return self._backend.group_get(params)
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
def getgroup(self, name, membership=False):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Return a bugzilla Group for the given name
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
:arg name: The group name used in bugzilla.
|
|
Michal Koutný |
ccf7f1 |
:raises XMLRPC Fault: Code 51 if the name does not exist
|
|
Michal Koutný |
ccf7f1 |
:raises XMLRPC Fault: Code 805 if the user does not have enough
|
|
Michal Koutný |
ccf7f1 |
permissions to view groups
|
|
Michal Koutný |
ccf7f1 |
:returns: Group record for the name
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
ret = self.getgroups(name, membership=membership)
|
|
Michal Koutný |
ccf7f1 |
return ret and ret[0]
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
def getgroups(self, grouplist, membership=False):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Return a list of Groups from .
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
:userlist: List of group names to lookup
|
|
Michal Koutný |
ccf7f1 |
:returns: List of Group records
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
grouplist = listify(grouplist)
|
|
Michal Koutný |
ccf7f1 |
groupobjs = [
|
|
Michal Koutný |
ccf7f1 |
Group(self, **rawgroup)
|
|
Michal Koutný |
ccf7f1 |
for rawgroup in self._getgroups(
|
|
Michal Koutný |
ccf7f1 |
names=grouplist, membership=membership).get('groups', [])
|
|
Michal Koutný |
ccf7f1 |
]
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
# Return in same order they were passed in
|
|
Michal Koutný |
ccf7f1 |
ret = []
|
|
Michal Koutný |
ccf7f1 |
for g in grouplist:
|
|
Michal Koutný |
ccf7f1 |
for gobj in groupobjs[:]:
|
|
Michal Koutný |
ccf7f1 |
if gobj.name == g:
|
|
Michal Koutný |
ccf7f1 |
groupobjs.remove(gobj)
|
|
Michal Koutný |
ccf7f1 |
ret.append(gobj)
|
|
Michal Koutný |
ccf7f1 |
break
|
|
Michal Koutný |
ccf7f1 |
ret += groupobjs
|
|
Michal Koutný |
ccf7f1 |
return ret
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
#############################
|
|
Michal Koutný |
ccf7f1 |
# ExternalBugs API wrappers #
|
|
Michal Koutný |
ccf7f1 |
#############################
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
def add_external_tracker(self, bug_ids, ext_bz_bug_id, ext_type_id=None,
|
|
Michal Koutný |
ccf7f1 |
ext_type_description=None, ext_type_url=None,
|
|
Michal Koutný |
ccf7f1 |
ext_status=None, ext_description=None,
|
|
Michal Koutný |
ccf7f1 |
ext_priority=None):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Wrapper method to allow adding of external tracking bugs using the
|
|
Michal Koutný |
ccf7f1 |
ExternalBugs::WebService::add_external_bug method.
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
This is documented at
|
|
Michal Koutný |
ccf7f1 |
https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#add-external-bug
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
bug_ids: A single bug id or list of bug ids to have external trackers
|
|
Michal Koutný |
ccf7f1 |
added.
|
|
Michal Koutný |
ccf7f1 |
ext_bz_bug_id: The external bug id (ie: the bug number in the
|
|
Michal Koutný |
ccf7f1 |
external tracker).
|
|
Michal Koutný |
ccf7f1 |
ext_type_id: The external tracker id as used by Bugzilla.
|
|
Michal Koutný |
ccf7f1 |
ext_type_description: The external tracker description as used by
|
|
Michal Koutný |
ccf7f1 |
Bugzilla.
|
|
Michal Koutný |
ccf7f1 |
ext_type_url: The external tracker url as used by Bugzilla.
|
|
Michal Koutný |
ccf7f1 |
ext_status: The status of the external bug.
|
|
Michal Koutný |
ccf7f1 |
ext_description: The description of the external bug.
|
|
Michal Koutný |
ccf7f1 |
ext_priority: The priority of the external bug.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
param_dict = {'ext_bz_bug_id': ext_bz_bug_id}
|
|
Michal Koutný |
ccf7f1 |
if ext_type_id is not None:
|
|
Michal Koutný |
ccf7f1 |
param_dict['ext_type_id'] = ext_type_id
|
|
Michal Koutný |
ccf7f1 |
if ext_type_description is not None:
|
|
Michal Koutný |
ccf7f1 |
param_dict['ext_type_description'] = ext_type_description
|
|
Michal Koutný |
ccf7f1 |
if ext_type_url is not None:
|
|
Michal Koutný |
ccf7f1 |
param_dict['ext_type_url'] = ext_type_url
|
|
Michal Koutný |
ccf7f1 |
if ext_status is not None:
|
|
Michal Koutný |
ccf7f1 |
param_dict['ext_status'] = ext_status
|
|
Michal Koutný |
ccf7f1 |
if ext_description is not None:
|
|
Michal Koutný |
ccf7f1 |
param_dict['ext_description'] = ext_description
|
|
Michal Koutný |
ccf7f1 |
if ext_priority is not None:
|
|
Michal Koutný |
ccf7f1 |
param_dict['ext_priority'] = ext_priority
|
|
Michal Koutný |
ccf7f1 |
params = {
|
|
Michal Koutný |
ccf7f1 |
'bug_ids': listify(bug_ids),
|
|
Michal Koutný |
ccf7f1 |
'external_bugs': [param_dict],
|
|
Michal Koutný |
ccf7f1 |
}
|
|
Michal Koutný |
ccf7f1 |
return self._backend.externalbugs_add(params)
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
def update_external_tracker(self, ids=None, ext_type_id=None,
|
|
Michal Koutný |
ccf7f1 |
ext_type_description=None, ext_type_url=None,
|
|
Michal Koutný |
ccf7f1 |
ext_bz_bug_id=None, bug_ids=None,
|
|
Michal Koutný |
ccf7f1 |
ext_status=None, ext_description=None,
|
|
Michal Koutný |
ccf7f1 |
ext_priority=None):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Wrapper method to allow adding of external tracking bugs using the
|
|
Michal Koutný |
ccf7f1 |
ExternalBugs::WebService::update_external_bug method.
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
This is documented at
|
|
Michal Koutný |
ccf7f1 |
https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#update-external-bug
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
ids: A single external tracker bug id or list of external tracker bug
|
|
Michal Koutný |
ccf7f1 |
ids.
|
|
Michal Koutný |
ccf7f1 |
ext_type_id: The external tracker id as used by Bugzilla.
|
|
Michal Koutný |
ccf7f1 |
ext_type_description: The external tracker description as used by
|
|
Michal Koutný |
ccf7f1 |
Bugzilla.
|
|
Michal Koutný |
ccf7f1 |
ext_type_url: The external tracker url as used by Bugzilla.
|
|
Michal Koutný |
ccf7f1 |
ext_bz_bug_id: A single external bug id or list of external bug ids
|
|
Michal Koutný |
ccf7f1 |
(ie: the bug number in the external tracker).
|
|
Michal Koutný |
ccf7f1 |
bug_ids: A single bug id or list of bug ids to have external tracker
|
|
Michal Koutný |
ccf7f1 |
info updated.
|
|
Michal Koutný |
ccf7f1 |
ext_status: The status of the external bug.
|
|
Michal Koutný |
ccf7f1 |
ext_description: The description of the external bug.
|
|
Michal Koutný |
ccf7f1 |
ext_priority: The priority of the external bug.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
params = {}
|
|
Michal Koutný |
ccf7f1 |
if ids is not None:
|
|
Michal Koutný |
ccf7f1 |
params['ids'] = listify(ids)
|
|
Michal Koutný |
ccf7f1 |
if ext_type_id is not None:
|
|
Michal Koutný |
ccf7f1 |
params['ext_type_id'] = ext_type_id
|
|
Michal Koutný |
ccf7f1 |
if ext_type_description is not None:
|
|
Michal Koutný |
ccf7f1 |
params['ext_type_description'] = ext_type_description
|
|
Michal Koutný |
ccf7f1 |
if ext_type_url is not None:
|
|
Michal Koutný |
ccf7f1 |
params['ext_type_url'] = ext_type_url
|
|
Michal Koutný |
ccf7f1 |
if ext_bz_bug_id is not None:
|
|
Michal Koutný |
ccf7f1 |
params['ext_bz_bug_id'] = listify(ext_bz_bug_id)
|
|
Michal Koutný |
ccf7f1 |
if bug_ids is not None:
|
|
Michal Koutný |
ccf7f1 |
params['bug_ids'] = listify(bug_ids)
|
|
Michal Koutný |
ccf7f1 |
if ext_status is not None:
|
|
Michal Koutný |
ccf7f1 |
params['ext_status'] = ext_status
|
|
Michal Koutný |
ccf7f1 |
if ext_description is not None:
|
|
Michal Koutný |
ccf7f1 |
params['ext_description'] = ext_description
|
|
Michal Koutný |
ccf7f1 |
if ext_priority is not None:
|
|
Michal Koutný |
ccf7f1 |
params['ext_priority'] = ext_priority
|
|
Michal Koutný |
ccf7f1 |
return self._backend.externalbugs_update(params)
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
def remove_external_tracker(self, ids=None, ext_type_id=None,
|
|
Michal Koutný |
ccf7f1 |
ext_type_description=None, ext_type_url=None,
|
|
Michal Koutný |
ccf7f1 |
ext_bz_bug_id=None, bug_ids=None):
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
Wrapper method to allow removal of external tracking bugs using the
|
|
Michal Koutný |
ccf7f1 |
ExternalBugs::WebService::remove_external_bug method.
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
This is documented at
|
|
Michal Koutný |
ccf7f1 |
https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#remove-external-bug
|
|
Michal Koutný |
ccf7f1 |
|
|
Michal Koutný |
ccf7f1 |
ids: A single external tracker bug id or list of external tracker bug
|
|
Michal Koutný |
ccf7f1 |
ids.
|
|
Michal Koutný |
ccf7f1 |
ext_type_id: The external tracker id as used by Bugzilla.
|
|
Michal Koutný |
ccf7f1 |
ext_type_description: The external tracker description as used by
|
|
Michal Koutný |
ccf7f1 |
Bugzilla.
|
|
Michal Koutný |
ccf7f1 |
ext_type_url: The external tracker url as used by Bugzilla.
|
|
Michal Koutný |
ccf7f1 |
ext_bz_bug_id: A single external bug id or list of external bug ids
|
|
Michal Koutný |
ccf7f1 |
(ie: the bug number in the external tracker).
|
|
Michal Koutný |
ccf7f1 |
bug_ids: A single bug id or list of bug ids to have external tracker
|
|
Michal Koutný |
ccf7f1 |
info updated.
|
|
Michal Koutný |
ccf7f1 |
"""
|
|
Michal Koutný |
ccf7f1 |
params = {}
|
|
Michal Koutný |
ccf7f1 |
if ids is not None:
|
|
Michal Koutný |
ccf7f1 |
params['ids'] = listify(ids)
|
|
Michal Koutný |
ccf7f1 |
if ext_type_id is not None:
|
|
Michal Koutný |
ccf7f1 |
params['ext_type_id'] = ext_type_id
|
|
Michal Koutný |
ccf7f1 |
if ext_type_description is not None:
|
|
Michal Koutný |
ccf7f1 |
params['ext_type_description'] = ext_type_description
|
|
Michal Koutný |
ccf7f1 |
if ext_type_url is not None:
|
|
Michal Koutný |
ccf7f1 |
params['ext_type_url'] = ext_type_url
|
|
Michal Koutný |
ccf7f1 |
if ext_bz_bug_id is not None:
|
|
Michal Koutný |
ccf7f1 |
params['ext_bz_bug_id'] = listify(ext_bz_bug_id)
|
|
Michal Koutný |
ccf7f1 |
if bug_ids is not None:
|
|
Michal Koutný |
ccf7f1 |
params['bug_ids'] = listify(bug_ids)
|
|
Michal Koutný |
ccf7f1 |
return self._backend.externalbugs_remove(params)
|