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)