1# python-bugzilla - a Python interface to bugzilla using xmlrpclib.
+ 2#
+ 3# Copyright (C) 2007, 2008 Red Hat Inc.
+ 4# Author: Will Woods <wwoods@redhat.com>
+ 5#
+ 6# This work is licensed under the GNU GPLv2 or later.
+ 7# See the COPYING file in the top-level directory.
+ 8
+ 9from.apiversionimportversion,__version__
+10from.baseimportBugzilla
+11from.exceptionsimportBugzillaError
+12from.oldclassesimport(Bugzilla3,Bugzilla32,Bugzilla34,Bugzilla36,
+13Bugzilla4,Bugzilla42,Bugzilla44,
+14NovellBugzilla,RHBugzilla,RHBugzilla3,RHBugzilla4)
+15
+16
+17# This is the public API. If you are explicitly instantiating any other
+18# class, using some function, or poking into internal files, don't complain
+19# if things break on you.
+20__all__=[
+21"Bugzilla3","Bugzilla32","Bugzilla34","Bugzilla36",
+22"Bugzilla4","Bugzilla42","Bugzilla44",
+23"NovellBugzilla",
+24"RHBugzilla3","RHBugzilla4","RHBugzilla",
+25'BugzillaError',
+26'Bugzilla',"version",
+27]
+28
+29
+30# Clear all other locals() from the public API
+31for__syminlocals().copy():
+32if__sym.startswith("__")or__symin__all__:
+33continue
+34locals().pop(__sym)
+35locals().pop("__sym")
+
The main API object. Connects to a bugzilla instance over XMLRPC, and
+provides wrapper functions to simplify dealing with API calls.
+
+
The most common invocation here will just be with just a URL:
+
+
bzapi = Bugzilla("http://bugzilla.example.com")
+
+
+
If you have previously logged into that URL, and have cached login
+tokens, you will automatically be logged in. Otherwise to
+log in, you can either pass auth options to __init__, or call a login
+helper like interactive_login().
+
+
If you are not logged in, you won't be able to access restricted data like
+user email, or perform write actions like bug create/update. But simple
+querys will work correctly.
+
+
If you are unsure if you are logged in, you can check the .logged_in
+property.
+
+
Another way to specify auth credentials is via a 'bugzillarc' file.
+See readconfig() documentation for details.
The main API object. Connects to a bugzilla instance over XMLRPC, and
+provides wrapper functions to simplify dealing with API calls.
+
+
The most common invocation here will just be with just a URL:
+
+
bzapi = Bugzilla("http://bugzilla.example.com")
+
+
+
If you have previously logged into that URL, and have cached login
+tokens, you will automatically be logged in. Otherwise to
+log in, you can either pass auth options to __init__, or call a login
+helper like interactive_login().
+
+
If you are not logged in, you won't be able to access restricted data like
+user email, or perform write actions like bug create/update. But simple
+querys will work correctly.
+
+
If you are unsure if you are logged in, you can check the .logged_in
+property.
+
+
Another way to specify auth credentials is via a 'bugzillarc' file.
+See readconfig() documentation for details.
The main API object. Connects to a bugzilla instance over XMLRPC, and
+provides wrapper functions to simplify dealing with API calls.
+
+
The most common invocation here will just be with just a URL:
+
+
bzapi = Bugzilla("http://bugzilla.example.com")
+
+
+
If you have previously logged into that URL, and have cached login
+tokens, you will automatically be logged in. Otherwise to
+log in, you can either pass auth options to __init__, or call a login
+helper like interactive_login().
+
+
If you are not logged in, you won't be able to access restricted data like
+user email, or perform write actions like bug create/update. But simple
+querys will work correctly.
+
+
If you are unsure if you are logged in, you can check the .logged_in
+property.
+
+
Another way to specify auth credentials is via a 'bugzillarc' file.
+See readconfig() documentation for details.
The main API object. Connects to a bugzilla instance over XMLRPC, and
+provides wrapper functions to simplify dealing with API calls.
+
+
The most common invocation here will just be with just a URL:
+
+
bzapi = Bugzilla("http://bugzilla.example.com")
+
+
+
If you have previously logged into that URL, and have cached login
+tokens, you will automatically be logged in. Otherwise to
+log in, you can either pass auth options to __init__, or call a login
+helper like interactive_login().
+
+
If you are not logged in, you won't be able to access restricted data like
+user email, or perform write actions like bug create/update. But simple
+querys will work correctly.
+
+
If you are unsure if you are logged in, you can check the .logged_in
+property.
+
+
Another way to specify auth credentials is via a 'bugzillarc' file.
+See readconfig() documentation for details.
The main API object. Connects to a bugzilla instance over XMLRPC, and
+provides wrapper functions to simplify dealing with API calls.
+
+
The most common invocation here will just be with just a URL:
+
+
bzapi = Bugzilla("http://bugzilla.example.com")
+
+
+
If you have previously logged into that URL, and have cached login
+tokens, you will automatically be logged in. Otherwise to
+log in, you can either pass auth options to __init__, or call a login
+helper like interactive_login().
+
+
If you are not logged in, you won't be able to access restricted data like
+user email, or perform write actions like bug create/update. But simple
+querys will work correctly.
+
+
If you are unsure if you are logged in, you can check the .logged_in
+property.
+
+
Another way to specify auth credentials is via a 'bugzillarc' file.
+See readconfig() documentation for details.
The main API object. Connects to a bugzilla instance over XMLRPC, and
+provides wrapper functions to simplify dealing with API calls.
+
+
The most common invocation here will just be with just a URL:
+
+
bzapi = Bugzilla("http://bugzilla.example.com")
+
+
+
If you have previously logged into that URL, and have cached login
+tokens, you will automatically be logged in. Otherwise to
+log in, you can either pass auth options to __init__, or call a login
+helper like interactive_login().
+
+
If you are not logged in, you won't be able to access restricted data like
+user email, or perform write actions like bug create/update. But simple
+querys will work correctly.
+
+
If you are unsure if you are logged in, you can check the .logged_in
+property.
+
+
Another way to specify auth credentials is via a 'bugzillarc' file.
+See readconfig() documentation for details.
The main API object. Connects to a bugzilla instance over XMLRPC, and
+provides wrapper functions to simplify dealing with API calls.
+
+
The most common invocation here will just be with just a URL:
+
+
bzapi = Bugzilla("http://bugzilla.example.com")
+
+
+
If you have previously logged into that URL, and have cached login
+tokens, you will automatically be logged in. Otherwise to
+log in, you can either pass auth options to __init__, or call a login
+helper like interactive_login().
+
+
If you are not logged in, you won't be able to access restricted data like
+user email, or perform write actions like bug create/update. But simple
+querys will work correctly.
+
+
If you are unsure if you are logged in, you can check the .logged_in
+property.
+
+
Another way to specify auth credentials is via a 'bugzillarc' file.
+See readconfig() documentation for details.
The main API object. Connects to a bugzilla instance over XMLRPC, and
+provides wrapper functions to simplify dealing with API calls.
+
+
The most common invocation here will just be with just a URL:
+
+
bzapi = Bugzilla("http://bugzilla.example.com")
+
+
+
If you have previously logged into that URL, and have cached login
+tokens, you will automatically be logged in. Otherwise to
+log in, you can either pass auth options to __init__, or call a login
+helper like interactive_login().
+
+
If you are not logged in, you won't be able to access restricted data like
+user email, or perform write actions like bug create/update. But simple
+querys will work correctly.
+
+
If you are unsure if you are logged in, you can check the .logged_in
+property.
+
+
Another way to specify auth credentials is via a 'bugzillarc' file.
+See readconfig() documentation for details.
Helper class for historical bugzilla.redhat.com back compat
+
+
Historically this class used many more non-upstream methods, but
+in 2012 RH started dropping most of its custom bits. By that time,
+upstream BZ had most of the important functionality.
+
+
Much of the remaining code here is just trying to keep things operating
+in python-bugzilla back compatible manner.
Helper class for historical bugzilla.redhat.com back compat
+
+
Historically this class used many more non-upstream methods, but
+in 2012 RH started dropping most of its custom bits. By that time,
+upstream BZ had most of the important functionality.
+
+
Much of the remaining code here is just trying to keep things operating
+in python-bugzilla back compatible manner.
43classRHBugzilla(Bugzilla):
+44"""
+45 Helper class for historical bugzilla.redhat.com back compat
+46
+47 Historically this class used many more non-upstream methods, but
+48 in 2012 RH started dropping most of its custom bits. By that time,
+49 upstream BZ had most of the important functionality.
+50
+51 Much of the remaining code here is just trying to keep things operating
+52 in python-bugzilla back compatible manner.
+53
+54 This class was written using bugzilla.redhat.com's API docs:
+55 https://bugzilla.redhat.com/docs/en/html/api/
+56 """
+57_is_redhat_bugzilla=True
+
+
+
+
Helper class for historical bugzilla.redhat.com back compat
+
+
Historically this class used many more non-upstream methods, but
+in 2012 RH started dropping most of its custom bits. By that time,
+upstream BZ had most of the important functionality.
+
+
Much of the remaining code here is just trying to keep things operating
+in python-bugzilla back compatible manner.
+
+ class
+ BugzillaError(builtins.Exception):
+
+
+
+
+
+
7classBugzillaError(Exception):
+ 8"""
+ 9 Error raised in the Bugzilla client code.
+10 """
+11@staticmethod
+12defget_bugzilla_error_string(exc):
+13"""
+14 Helper to return the bugzilla instance error message from an
+15 XMLRPC Fault, or any other exception type that's raised from bugzilla
+16 interaction
+17 """
+18returngetattr(exc,"faultString",str(exc))
+19
+20@staticmethod
+21defget_bugzilla_error_code(exc):
+22"""
+23 Helper to return the bugzilla instance error code from an
+24 XMLRPC Fault, or any other exception type that's raised from bugzilla
+25 interaction
+26 """
+27forpropnamein["faultCode","code"]:
+28ifhasattr(exc,propname):
+29returngetattr(exc,propname)
+30returnNone
+31
+32def__init__(self,message,code=None):
+33"""
+34 :param code: The error code from the remote bugzilla instance. Only
+35 set if the error came directly from the remove bugzilla
+36 """
+37self.code=code
+38ifself.code:
+39message+=" (code=%s)"%self.code
+40Exception.__init__(self,message)
+
+
+
+
Error raised in the Bugzilla client code.
+
+
+
+
+
+
+
+ BugzillaError(message, code=None)
+
+
+
+
+
+
32def__init__(self,message,code=None):
+33"""
+34 :param code: The error code from the remote bugzilla instance. Only
+35 set if the error came directly from the remove bugzilla
+36 """
+37self.code=code
+38ifself.code:
+39message+=" (code=%s)"%self.code
+40Exception.__init__(self,message)
+
+
+
+
Parameters
+
+
+
code: The error code from the remote bugzilla instance. Only
+set if the error came directly from the remove bugzilla
+
+
+
+
+
+
+
+
+
@staticmethod
+
+ def
+ get_bugzilla_error_string(exc):
+
+
+
+
+
+
11@staticmethod
+12defget_bugzilla_error_string(exc):
+13"""
+14 Helper to return the bugzilla instance error message from an
+15 XMLRPC Fault, or any other exception type that's raised from bugzilla
+16 interaction
+17 """
+18returngetattr(exc,"faultString",str(exc))
+
+
+
+
Helper to return the bugzilla instance error message from an
+XMLRPC Fault, or any other exception type that's raised from bugzilla
+interaction
+
+
+
+
+
+
+
+
@staticmethod
+
+ def
+ get_bugzilla_error_code(exc):
+
+
+
+
+
+
20@staticmethod
+21defget_bugzilla_error_code(exc):
+22"""
+23 Helper to return the bugzilla instance error code from an
+24 XMLRPC Fault, or any other exception type that's raised from bugzilla
+25 interaction
+26 """
+27forpropnamein["faultCode","code"]:
+28ifhasattr(exc,propname):
+29returngetattr(exc,propname)
+30returnNone
+
+
+
+
Helper to return the bugzilla instance error code from an
+XMLRPC Fault, or any other exception type that's raised from bugzilla
+interaction
+
+
+
+
+
+
+ code
+
+
+
+
+
+
+
+
+
+
+
+
+
+ class
+ Bugzilla:
+
+
+
+
+
+
80classBugzilla(object):
+ 81"""
+ 82 The main API object. Connects to a bugzilla instance over XMLRPC, and
+ 83 provides wrapper functions to simplify dealing with API calls.
+ 84
+ 85 The most common invocation here will just be with just a URL:
+ 86
+ 87 bzapi = Bugzilla("http://bugzilla.example.com")
+ 88
+ 89 If you have previously logged into that URL, and have cached login
+ 90 tokens, you will automatically be logged in. Otherwise to
+ 91 log in, you can either pass auth options to __init__, or call a login
+ 92 helper like interactive_login().
+ 93
+ 94 If you are not logged in, you won't be able to access restricted data like
+ 95 user email, or perform write actions like bug create/update. But simple
+ 96 querys will work correctly.
+ 97
+ 98 If you are unsure if you are logged in, you can check the .logged_in
+ 99 property.
+ 100
+ 101 Another way to specify auth credentials is via a 'bugzillarc' file.
+ 102 See readconfig() documentation for details.
+ 103 """
+ 104@staticmethod
+ 105defurl_to_query(url):
+ 106"""
+ 107 Given a big huge bugzilla query URL, returns a query dict that can
+ 108 be passed along to the Bugzilla.query() method.
+ 109 """
+ 110q={}
+ 111
+ 112# pylint: disable=unpacking-non-sequence
+ 113(ignore1,ignore2,path,
+ 114ignore,query,ignore3)=urllib.parse.urlparse(url)
+ 115
+ 116base=os.path.basename(path)
+ 117ifbasenotin('buglist.cgi','query.cgi'):
+ 118return{}
+ 119
+ 120for(k,v)inurllib.parse.parse_qsl(query):
+ 121ifknotinq:
+ 122q[k]=v
+ 123elifisinstance(q[k],list):
+ 124q[k].append(v)
+ 125else:
+ 126oldv=q[k]
+ 127q[k]=[oldv,v]
+ 128
+ 129# Handle saved searches
+ 130ifbase=="buglist.cgi"and"namedcmd"inqand"sharer_id"inq:
+ 131q={
+ 132"sharer_id":q["sharer_id"],
+ 133"savedsearch":q["namedcmd"],
+ 134}
+ 135
+ 136returnq
+ 137
+ 138@staticmethod
+ 139deffix_url(url,force_rest=False):
+ 140"""
+ 141 Turn passed url into a bugzilla XMLRPC web url
+ 142
+ 143 :param force_rest: If True, generate a REST API url
+ 144 """
+ 145(scheme,netloc,path,
+ 146params,query,fragment)=urllib.parse.urlparse(url)
+ 147ifnotscheme:
+ 148scheme='https'
+ 149
+ 150ifpathandnotnetloc:
+ 151netloc=path.split("/",1)[0]
+ 152path="/".join(path.split("/")[1:])orNone
+ 153
+ 154ifnotpath:
+ 155path='xmlrpc.cgi'
+ 156ifforce_rest:
+ 157path="rest/"
+ 158
+ 159ifnotpath.startswith("/"):
+ 160path="/"+path
+ 161
+ 162newurl=urllib.parse.urlunparse(
+ 163(scheme,netloc,path,params,query,fragment))
+ 164returnnewurl
+ 165
+ 166@staticmethod
+ 167defget_rcfile_default_url():
+ 168"""
+ 169 Helper to check all the default bugzillarc file paths for
+ 170 a [DEFAULT] url=X section, and if found, return it.
+ 171 """
+ 172configpaths=_BugzillaRCFile.get_default_configpaths()
+ 173rcfile=_BugzillaRCFile()
+ 174rcfile.set_configpaths(configpaths)
+ 175returnrcfile.get_default_url()
+ 176
+ 177
+ 178def__init__(self,url=-1,user=None,password=None,cookiefile=-1,
+ 179sslverify=True,tokenfile=-1,use_creds=True,api_key=None,
+ 180cert=None,configpaths=-1,
+ 181force_rest=False,force_xmlrpc=False,requests_session=None):
+ 182"""
+ 183 :param url: The bugzilla instance URL, which we will connect
+ 184 to immediately. Most users will want to specify this at
+ 185 __init__ time, but you can defer connecting by passing
+ 186 url=None and calling connect(URL) manually
+ 187 :param user: optional username to connect with
+ 188 :param password: optional password for the connecting user
+ 189 :param cert: optional certificate file for client side certificate
+ 190 authentication
+ 191 :param cookiefile: Deprecated, raises an error if not -1 or None
+ 192 :param sslverify: Set this to False to skip SSL hostname and CA
+ 193 validation checks, like out of date certificate
+ 194 :param tokenfile: Location to cache the API login token so youi
+ 195 don't have to keep specifying username/password.
+ 196 If -1, use the default path. If None, don't use
+ 197 or save any tokenfile.
+ 198 :param use_creds: If False, this disables tokenfile
+ 199 and configpaths by default. This is a convenience option to
+ 200 unset those values at init time. If those values are later
+ 201 changed, they may be used for future operations.
+ 202 :param sslverify: Maps to 'requests' sslverify parameter. Set to
+ 203 False to disable SSL verification, but it can also be a path
+ 204 to file or directory for custom certs.
+ 205 :param api_key: A bugzilla5+ API key
+ 206 :param configpaths: A list of possible bugzillarc locations.
+ 207 :param force_rest: Force use of the REST API
+ 208 :param force_xmlrpc: Force use of the XMLRPC API. If neither force_X
+ 209 parameter are specified, heuristics will be used to determine
+ 210 which API to use, with XMLRPC preferred for back compatability.
+ 211 :param requests_session: An optional requests.Session object the
+ 212 API will use to contact the remote bugzilla instance. This
+ 213 way the API user can set up whatever auth bits they may need.
+ 214 """
+ 215ifurl==-1:
+ 216raiseTypeError("Specify a valid bugzilla url, or pass url=None")
+ 217
+ 218# Settings the user might want to tweak
+ 219self.user=useror''
+ 220self.password=passwordor''
+ 221self.api_key=api_key
+ 222self.cert=certorNone
+ 223self.url=''
+ 224
+ 225self._backend=None
+ 226self._session=None
+ 227self._user_requests_session=requests_session
+ 228self._sslverify=sslverify
+ 229self._cache=_BugzillaAPICache()
+ 230self._bug_autorefresh=False
+ 231self._is_redhat_bugzilla=False
+ 232
+ 233self._rcfile=_BugzillaRCFile()
+ 234self._tokencache=_BugzillaTokenCache()
+ 235
+ 236self._force_rest=force_rest
+ 237self._force_xmlrpc=force_xmlrpc
+ 238
+ 239ifcookiefilenotin[None,-1]:
+ 240raiseTypeError("cookiefile is deprecated, don't pass any value.")
+ 241
+ 242ifnotuse_creds:
+ 243tokenfile=None
+ 244configpaths=[]
+ 245
+ 246iftokenfile==-1:
+ 247tokenfile=self._tokencache.get_default_path()
+ 248ifconfigpaths==-1:
+ 249configpaths=_BugzillaRCFile.get_default_configpaths()
+ 250
+ 251self._settokenfile(tokenfile)
+ 252self._setconfigpath(configpaths)
+ 253
+ 254ifurl:
+ 255self.connect(url)
+ 256
+ 257def_detect_is_redhat_bugzilla(self):
+ 258ifself._is_redhat_bugzilla:
+ 259returnTrue
+ 260
+ 261match=".redhat.com"
+ 262ifmatchinself.url:
+ 263log.info("Using RHBugzilla for URL containing %s",match)
+ 264returnTrue
+ 265
+ 266returnFalse
+ 267
+ 268def_init_class_from_url(self):
+ 269"""
+ 270 Detect if we should use RHBugzilla class, and if so, set it
+ 271 """
+ 272from.oldclassesimportRHBugzilla# pylint: disable=cyclic-import
+ 273
+ 274ifnotself._detect_is_redhat_bugzilla():
+ 275return
+ 276
+ 277self._is_redhat_bugzilla=True
+ 278ifself.__class__==Bugzilla:
+ 279# Overriding the class doesn't have any functional effect,
+ 280# but we continue to do it for API back compat incase anyone
+ 281# is doing any class comparison. We should drop this in the future
+ 282self.__class__=RHBugzilla
+ 283
+ 284def_get_field_aliases(self):
+ 285# List of field aliases. Maps old style RHBZ parameter
+ 286# names to actual upstream values. Used for createbug() and
+ 287# query include_fields at least.
+ 288ret=[]
+ 289
+ 290def_add(*args,**kwargs):
+ 291ret.append(_FieldAlias(*args,**kwargs))
+ 292
+ 293def_add_both(newname,origname):
+ 294_add(newname,origname,is_api=False)
+ 295_add(origname,newname,is_bug=False)
+ 296
+ 297_add('summary','short_desc')
+ 298_add('description','comment')
+ 299_add('platform','rep_platform')
+ 300_add('severity','bug_severity')
+ 301_add('status','bug_status')
+ 302_add('id','bug_id')
+ 303_add('blocks','blockedby')
+ 304_add('blocks','blocked')
+ 305_add('depends_on','dependson')
+ 306_add('creator','reporter')
+ 307_add('url','bug_file_loc')
+ 308_add('dupe_of','dupe_id')
+ 309_add('dupe_of','dup_id')
+ 310_add('comments','longdescs')
+ 311_add('creation_time','opendate')
+ 312_add('creation_time','creation_ts')
+ 313_add('whiteboard','status_whiteboard')
+ 314_add('last_change_time','delta_ts')
+ 315
+ 316ifself._is_redhat_bugzilla:
+ 317_add_both('fixed_in','cf_fixed_in')
+ 318_add_both('qa_whiteboard','cf_qa_whiteboard')
+ 319_add_both('devel_whiteboard','cf_devel_whiteboard')
+ 320_add_both('internal_whiteboard','cf_internal_whiteboard')
+ 321
+ 322_add('component','components',is_bug=False)
+ 323_add('version','versions',is_bug=False)
+ 324# Yes, sub_components is the field name the API expects
+ 325_add('sub_components','sub_component',is_bug=False)
+ 326# flags format isn't exactly the same but it's the closest approx
+ 327_add('flags','flag_types')
+ 328
+ 329returnret
+ 330
+ 331def_get_user_agent(self):
+ 332return'python-bugzilla/%s'%__version__
+ 333user_agent=property(_get_user_agent)
+ 334
+ 335@property
+ 336defbz_ver_major(self):
+ 337returnself._cache.version_parsed[0]
+ 338
+ 339@property
+ 340defbz_ver_minor(self):
+ 341returnself._cache.version_parsed[1]
+ 342
+ 343
+ 344###################
+ 345# Private helpers #
+ 346###################
+ 347
+ 348def_get_version(self):
+ 349"""
+ 350 Return version number as a float
+ 351 """
+ 352returnfloat("%d.%d"%(self.bz_ver_major,self.bz_ver_minor))
+ 353
+ 354def_get_bug_aliases(self):
+ 355return[(f.newname,f.oldname)
+ 356forfinself._get_field_aliases()iff.is_bug]
+ 357
+ 358def_get_api_aliases(self):
+ 359return[(f.newname,f.oldname)
+ 360forfinself._get_field_aliases()iff.is_api]
+ 361
+ 362
+ 363#################
+ 364# Auth handling #
+ 365#################
+ 366
+ 367def_getcookiefile(self):
+ 368returnNone
+ 369cookiefile=property(_getcookiefile)
+ 370
+ 371def_gettokenfile(self):
+ 372returnself._tokencache.get_filename()
+ 373def_settokenfile(self,filename):
+ 374self._tokencache.set_filename(filename)
+ 375def_deltokenfile(self):
+ 376self._settokenfile(None)
+ 377tokenfile=property(_gettokenfile,_settokenfile,_deltokenfile)
+ 378
+ 379def_getconfigpath(self):
+ 380returnself._rcfile.get_configpaths()
+ 381def_setconfigpath(self,configpaths):
+ 382returnself._rcfile.set_configpaths(configpaths)
+ 383def_delconfigpath(self):
+ 384returnself._rcfile.set_configpaths(None)
+ 385configpath=property(_getconfigpath,_setconfigpath,_delconfigpath)
+ 386
+ 387
+ 388#############################
+ 389# Login/connection handling #
+ 390#############################
+ 391
+ 392defreadconfig(self,configpath=None,overwrite=True):
+ 393"""
+ 394 :param configpath: Optional bugzillarc path to read, instead of
+ 395 the default list.
+ 396
+ 397 This function is called automatically from Bugzilla connect(), which
+ 398 is called at __init__ if a URL is passed. Calling it manually is
+ 399 just for passing in a non-standard configpath.
+ 400
+ 401 The locations for the bugzillarc file are preferred in this order:
+ 402
+ 403 ~/.config/python-bugzilla/bugzillarc
+ 404 ~/.bugzillarc
+ 405 /etc/bugzillarc
+ 406
+ 407 It has content like:
+ 408 [bugzilla.yoursite.com]
+ 409 user = username
+ 410 password = password
+ 411 Or
+ 412 [bugzilla.yoursite.com]
+ 413 api_key = key
+ 414
+ 415 The file can have multiple sections for different bugzilla instances.
+ 416 A 'url' field in the [DEFAULT] section can be used to set a default
+ 417 URL for the bugzilla command line tool.
+ 418
+ 419 Be sure to set appropriate permissions on bugzillarc if you choose to
+ 420 store your password in it!
+ 421
+ 422 :param overwrite: If True, bugzillarc will clobber any already
+ 423 set self.user/password/api_key/cert value.
+ 424 """
+ 425ifconfigpath:
+ 426self._setconfigpath(configpath)
+ 427data=self._rcfile.parse(self.url)
+ 428
+ 429forkey,valindata.items():
+ 430ifkey=="api_key"and(overwriteornotself.api_key):
+ 431log.debug("bugzillarc: setting api_key")
+ 432self.api_key=val
+ 433elifkey=="user"and(overwriteornotself.user):
+ 434log.debug("bugzillarc: setting user=%s",val)
+ 435self.user=val
+ 436elifkey=="password"and(overwriteornotself.password):
+ 437log.debug("bugzillarc: setting password")
+ 438self.password=val
+ 439elifkey=="cert"and(overwriteornotself.cert):
+ 440log.debug("bugzillarc: setting cert")
+ 441self.cert=val
+ 442else:
+ 443log.debug("bugzillarc: unknown key=%s",key)
+ 444
+ 445def_set_bz_version(self,version):
+ 446self._cache.version_raw=version
+ 447try:
+ 448major,minor=[int(i)foriinversion.split(".")[0:2]]
+ 449exceptException:
+ 450log.debug("version doesn't match expected format X.Y.Z, "
+ 451"assuming 5.0",exc_info=True)
+ 452major=5
+ 453minor=0
+ 454self._cache.version_parsed=(major,minor)
+ 455
+ 456def_get_backend_class(self,url):# pragma: no cover
+ 457# This is a hook for the test suite to do some mock hackery
+ 458ifself._force_restandself._force_xmlrpc:
+ 459raiseBugzillaError(
+ 460"Cannot specify both force_rest and force_xmlrpc")
+ 461
+ 462xmlurl=self.fix_url(url)
+ 463ifself._force_xmlrpc:
+ 464return_BackendXMLRPC,xmlurl
+ 465
+ 466resturl=self.fix_url(url,force_rest=self._force_rest)
+ 467ifself._force_rest:
+ 468return_BackendREST,resturl
+ 469
+ 470# Simple heuristic if the original url has a path in it
+ 471if"/xmlrpc"inurl:
+ 472return_BackendXMLRPC,xmlurl
+ 473if"/rest"inurl:
+ 474return_BackendREST,resturl
+ 475
+ 476# We were passed something like bugzilla.example.com but we
+ 477# aren't sure which method to use, try probing
+ 478if_BackendXMLRPC.probe(xmlurl):
+ 479return_BackendXMLRPC,xmlurl
+ 480if_BackendREST.probe(resturl):
+ 481return_BackendREST,resturl
+ 482
+ 483# Otherwise fallback to XMLRPC default and let it fail
+ 484return_BackendXMLRPC,xmlurl
+ 485
+ 486defconnect(self,url=None):
+ 487"""
+ 488 Connect to the bugzilla instance with the given url. This is
+ 489 called by __init__ if a URL is passed. Or it can be called manually
+ 490 at any time with a passed URL.
+ 491
+ 492 This will also read any available config files (see readconfig()),
+ 493 which may set 'user' and 'password', and others.
+ 494
+ 495 If 'user' and 'password' are both set, we'll run login(). Otherwise
+ 496 you'll have to login() yourself before some methods will work.
+ 497 """
+ 498ifself._session:
+ 499self.disconnect()
+ 500
+ 501url=urlorself.url
+ 502backendclass,newurl=self._get_backend_class(url)
+ 503ifurl!=newurl:
+ 504log.debug("Converted url=%s to fixed url=%s",url,newurl)
+ 505self.url=newurl
+ 506log.debug("Connecting with URL %s",self.url)
+ 507
+ 508# we've changed URLs - reload config
+ 509self.readconfig(overwrite=False)
+ 510
+ 511# Detect if connecting to redhat bugzilla
+ 512self._init_class_from_url()
+ 513
+ 514self._session=_BugzillaSession(self.url,self.user_agent,
+ 515sslverify=self._sslverify,
+ 516cert=self.cert,
+ 517tokencache=self._tokencache,
+ 518api_key=self.api_key,
+ 519is_redhat_bugzilla=self._is_redhat_bugzilla,
+ 520requests_session=self._user_requests_session)
+ 521self._backend=backendclass(self.url,self._session)
+ 522
+ 523if(self.userandself.password):
+ 524log.info("user and password present - doing login()")
+ 525self.login()
+ 526
+ 527ifself.api_key:
+ 528log.debug("using API key")
+ 529
+ 530version=self._backend.bugzilla_version()["version"]
+ 531log.debug("Bugzilla version string: %s",version)
+ 532self._set_bz_version(version)
+ 533
+ 534
+ 535@property
+ 536def_proxy(self):
+ 537"""
+ 538 Return an xmlrpc ServerProxy instance that will work seamlessly
+ 539 with bugzilla
+ 540
+ 541 Some apps have historically accessed _proxy directly, like
+ 542 fedora infrastrucutre pieces. So we consider it part of the API
+ 543 """
+ 544returnself._backend.get_xmlrpc_proxy()
+ 545
+ 546defis_xmlrpc(self):
+ 547"""
+ 548 :returns: True if using the XMLRPC API
+ 549 """
+ 550returnself._backend.is_xmlrpc()
+ 551
+ 552defis_rest(self):
+ 553"""
+ 554 :returns: True if using the REST API
+ 555 """
+ 556returnself._backend.is_rest()
+ 557
+ 558defget_requests_session(self):
+ 559"""
+ 560 Give API users access to the Requests.session object we use for
+ 561 talking to the remote bugzilla instance.
+ 562
+ 563 :returns: The Requests.session object backing the open connection.
+ 564 """
+ 565returnself._session.get_requests_session()
+ 566
+ 567defdisconnect(self):
+ 568"""
+ 569 Disconnect from the given bugzilla instance.
+ 570 """
+ 571self._backend=None
+ 572self._session=None
+ 573self._cache=_BugzillaAPICache()
+ 574
+ 575deflogin(self,user=None,password=None,restrict_login=None):
+ 576"""
+ 577 Attempt to log in using the given username and password. Subsequent
+ 578 method calls will use this username and password. Returns False if
+ 579 login fails, otherwise returns some kind of login info - typically
+ 580 either a numeric userid, or a dict of user info.
+ 581
+ 582 If user is not set, the value of Bugzilla.user will be used. If *that*
+ 583 is not set, ValueError will be raised. If login fails, BugzillaError
+ 584 will be raised.
+ 585
+ 586 The login session can be restricted to current user IP address
+ 587 with restrict_login argument. (Bugzilla 4.4+)
+ 588
+ 589 This method will be called implicitly at the end of connect() if user
+ 590 and password are both set. So under most circumstances you won't need
+ 591 to call this yourself.
+ 592 """
+ 593ifself.api_key:
+ 594raiseValueError("cannot login when using an API key")
+ 595
+ 596ifuser:
+ 597self.user=user
+ 598ifpassword:
+ 599self.password=password
+ 600
+ 601ifnotself.user:
+ 602raiseValueError("missing username")
+ 603ifnotself.password:
+ 604raiseValueError("missing password")
+ 605
+ 606payload={"login":self.user}
+ 607ifrestrict_login:
+ 608payload['restrict_login']=True
+ 609log.debug("logging in with options %s",str(payload))
+ 610payload['password']=self.password
+ 611
+ 612try:
+ 613ret=self._backend.user_login(payload)
+ 614self.password=''
+ 615log.info("login succeeded for user=%s",self.user)
+ 616if"token"inret:
+ 617self._tokencache.set_value(self.url,ret["token"])
+ 618returnret
+ 619exceptExceptionase:
+ 620log.debug("Login exception: %s",str(e),exc_info=True)
+ 621raiseBugzillaError("Login failed: %s"%
+ 622BugzillaError.get_bugzilla_error_string(e))fromNone
+ 623
+ 624definteractive_save_api_key(self):
+ 625"""
+ 626 Helper method to interactively ask for an API key, verify it
+ 627 is valid, and save it to a bugzillarc file referenced via
+ 628 self.configpaths
+ 629 """
+ 630sys.stdout.write('API Key: ')
+ 631sys.stdout.flush()
+ 632api_key=sys.stdin.readline().strip()
+ 633
+ 634self.disconnect()
+ 635self.api_key=api_key
+ 636
+ 637log.info('Checking API key... ')
+ 638self.connect()
+ 639
+ 640ifnotself.logged_in:# pragma: no cover
+ 641raiseBugzillaError("Login with API_KEY failed")
+ 642log.info('API Key accepted')
+ 643
+ 644wrote_filename=self._rcfile.save_api_key(self.url,self.api_key)
+ 645log.info("API key written to filename=%s",wrote_filename)
+ 646
+ 647msg="Login successful."
+ 648ifwrote_filename:
+ 649msg+=" API key written to %s"%wrote_filename
+ 650print(msg)
+ 651
+ 652definteractive_login(self,user=None,password=None,force=False,
+ 653restrict_login=None):
+ 654"""
+ 655 Helper method to handle login for this bugzilla instance.
+ 656
+ 657 :param user: bugzilla username. If not specified, prompt for it.
+ 658 :param password: bugzilla password. If not specified, prompt for it.
+ 659 :param force: Unused
+ 660 :param restrict_login: restricts session to IP address
+ 661 """
+ 662ignore=force
+ 663log.debug('Calling interactive_login')
+ 664
+ 665ifnotuser:
+ 666sys.stdout.write('Bugzilla Username: ')
+ 667sys.stdout.flush()
+ 668user=sys.stdin.readline().strip()
+ 669ifnotpassword:
+ 670password=getpass.getpass('Bugzilla Password: ')
+ 671
+ 672log.info('Logging in... ')
+ 673out=self.login(user,password,restrict_login)
+ 674msg="Login successful."
+ 675if"token"notinout:
+ 676msg+=" However no token was returned."
+ 677else:
+ 678ifnotself.tokenfile:
+ 679msg+=" Token not saved to disk."
+ 680else:
+ 681msg+=" Token cache saved to %s"%self.tokenfile
+ 682ifself._get_version()>=5.0:
+ 683msg+="\nToken usage is deprecated. "
+ 684msg+="Consider using bugzilla API keys instead. "
+ 685msg+="See `man bugzilla` for more details."
+ 686print(msg)
+ 687
+ 688deflogout(self):
+ 689"""
+ 690 Log out of bugzilla. Drops server connection and user info, and
+ 691 destroys authentication cache
+ 692 """
+ 693self._backend.user_logout()
+ 694self.disconnect()
+ 695self.user=''
+ 696self.password=''
+ 697
+ 698@property
+ 699deflogged_in(self):
+ 700"""
+ 701 This is True if this instance is logged in else False.
+ 702
+ 703 We test if this session is authenticated by calling the User.get()
+ 704 XMLRPC method with ids set. Logged-out users cannot pass the 'ids'
+ 705 parameter and will result in a 505 error. If we tried to login with a
+ 706 token, but the token was incorrect or expired, the server returns a
+ 707 32000 error.
+ 708
+ 709 For Bugzilla 5 and later, a new method, User.valid_login is available
+ 710 to test the validity of the token. However, this will require that the
+ 711 username be cached along with the token in order to work effectively in
+ 712 all scenarios and is not currently used. For more information, refer to
+ 713 the following url.
+ 714
+ 715 http://bugzilla.readthedocs.org/en/latest/api/core/v1/user.html#valid-login
+ 716 """
+ 717try:
+ 718self._backend.user_get({"ids":[1]})
+ 719returnTrue
+ 720exceptExceptionase:
+ 721code=BugzillaError.get_bugzilla_error_code(e)
+ 722ifcodein[505,32000]:
+ 723returnFalse
+ 724raisee
+ 725
+ 726
+ 727######################
+ 728# Bugfields querying #
+ 729######################
+ 730
+ 731defgetbugfields(self,force_refresh=False,names=None):
+ 732"""
+ 733 Calls getBugFields, which returns a list of fields in each bug
+ 734 for this bugzilla instance. This can be used to set the list of attrs
+ 735 on the Bug object.
+ 736
+ 737 :param force_refresh: If True, overwrite the bugfield cache
+ 738 with these newly checked values.
+ 739 :param names: Only check for the passed bug field names
+ 740 """
+ 741def_fieldnames():
+ 742data={"include_fields":["name"]}
+ 743ifnames:
+ 744data["names"]=names
+ 745r=self._backend.bug_fields(data)
+ 746return[f['name']forfinr['fields']]
+ 747
+ 748ifforce_refreshornotself._cache.bugfields:
+ 749log.debug("Refreshing bugfields")
+ 750self._cache.bugfields=_fieldnames()
+ 751self._cache.bugfields.sort()
+ 752log.debug("bugfields = %s",self._cache.bugfields)
+ 753
+ 754returnself._cache.bugfields
+ 755bugfields=property(fget=lambdaself:self.getbugfields(),
+ 756fdel=lambdaself:setattr(self,'_bugfields',None))
+ 757
+ 758
+ 759####################
+ 760# Product querying #
+ 761####################
+ 762
+ 763defproduct_get(self,ids=None,names=None,
+ 764include_fields=None,exclude_fields=None,
+ 765ptype=None):
+ 766"""
+ 767 Raw wrapper around Product.get
+ 768 https://bugzilla.readthedocs.io/en/latest/api/core/v1/product.html#get-product
+ 769
+ 770 This does not perform any caching like other product API calls.
+ 771 If ids, names, or ptype is not specified, we default to
+ 772 ptype=accessible for historical reasons
+ 773
+ 774 @ids: List of product IDs to lookup
+ 775 @names: List of product names to lookup
+ 776 @ptype: Either 'accessible', 'selectable', or 'enterable'. If
+ 777 specified, we return data for all those
+ 778 @include_fields: Only include these fields in the output
+ 779 @exclude_fields: Do not include these fields in the output
+ 780 """
+ 781ifidsisNoneandnamesisNoneandptypeisNone:
+ 782ptype="accessible"
+ 783
+ 784ifptype:
+ 785raw=None
+ 786ifptype=="accessible":
+ 787raw=self._backend.product_get_accessible()
+ 788elifptype=="enterable":
+ 789raw=self._backend.product_get_enterable()
+ 790elifptype=="selectable":
+ 791raw=self._backend.product_get_selectable()
+ 792
+ 793ifrawisNone:
+ 794raiseRuntimeError("Unknown ptype=%s"%ptype)
+ 795ids=raw['ids']
+ 796log.debug("For ptype=%s found ids=%s",ptype,ids)
+ 797
+ 798kwargs={}
+ 799ifids:
+ 800kwargs["ids"]=listify(ids)
+ 801ifnames:
+ 802kwargs["names"]=listify(names)
+ 803ifinclude_fields:
+ 804kwargs["include_fields"]=include_fields
+ 805ifexclude_fields:
+ 806kwargs["exclude_fields"]=exclude_fields
+ 807
+ 808ret=self._backend.product_get(kwargs)
+ 809returnret['products']
+ 810
+ 811defrefresh_products(self,**kwargs):
+ 812"""
+ 813 Refresh a product's cached info. Basically calls product_get
+ 814 with the passed arguments, and tries to intelligently update
+ 815 our product cache.
+ 816
+ 817 For example, if we already have cached info for product=foo,
+ 818 and you pass in names=["bar", "baz"], the new cache will have
+ 819 info for products foo, bar, baz. Individual product fields are
+ 820 also updated.
+ 821 """
+ 822forproductinself.product_get(**kwargs):
+ 823updated=False
+ 824forcurrentinself._cache.products[:]:
+ 825if(current.get("id",-1)!=product.get("id",-2)and
+ 826current.get("name",-1)!=product.get("name",-2)):
+ 827continue
+ 828
+ 829_nested_update(current,product)
+ 830updated=True
+ 831break
+ 832ifnotupdated:
+ 833self._cache.products.append(product)
+ 834
+ 835defgetproducts(self,force_refresh=False,**kwargs):
+ 836"""
+ 837 Query all products and return the raw dict info. Takes all the
+ 838 same arguments as product_get.
+ 839
+ 840 On first invocation this will contact bugzilla and internally
+ 841 cache the results. Subsequent getproducts calls or accesses to
+ 842 self.products will return this cached data only.
+ 843
+ 844 :param force_refresh: force refreshing via refresh_products()
+ 845 """
+ 846ifforce_refreshornotself._cache.products:
+ 847self.refresh_products(**kwargs)
+ 848returnself._cache.products
+ 849
+ 850products=property(
+ 851fget=lambdaself:self.getproducts(),
+ 852fdel=lambdaself:setattr(self,'_products',None),
+ 853doc="Helper for accessing the products cache. If nothing "
+ 854"has been cached yet, this calls getproducts()")
+ 855
+ 856
+ 857#######################
+ 858# components querying #
+ 859#######################
+ 860
+ 861def_lookup_product_in_cache(self,productname):
+ 862prodstr=isinstance(productname,str)andproductnameorNone
+ 863prodint=isinstance(productname,int)andproductnameorNone
+ 864forproddictinself._cache.products:
+ 865ifprodstr==proddict.get("name",-1):
+ 866returnproddict
+ 867ifprodint==proddict.get("id","nope"):
+ 868returnproddict
+ 869return{}
+ 870
+ 871defgetcomponentsdetails(self,product,force_refresh=False):
+ 872"""
+ 873 Wrapper around Product.get(include_fields=["components"]),
+ 874 returning only the "components" data for the requested product,
+ 875 slightly reworked to a dict mapping of components.name: components,
+ 876 for historical reasons.
+ 877
+ 878 This uses the product cache, but will update it if the product
+ 879 isn't found or "components" isn't cached for the product.
+ 880
+ 881 In cases like bugzilla.redhat.com where there are tons of
+ 882 components for some products, this API will time out. You
+ 883 should use product_get instead.
+ 884 """
+ 885proddict=self._lookup_product_in_cache(product)
+ 886
+ 887if(force_refreshornotproddictor"components"notinproddict):
+ 888self.refresh_products(names=[product],
+ 889include_fields=["name","id","components"])
+ 890proddict=self._lookup_product_in_cache(product)
+ 891
+ 892ret={}
+ 893forcompdictinproddict["components"]:
+ 894ret[compdict["name"]]=compdict
+ 895returnret
+ 896
+ 897defgetcomponentdetails(self,product,component,force_refresh=False):
+ 898"""
+ 899 Helper for accessing a single component's info. This is a wrapper
+ 900 around getcomponentsdetails, see that for explanation
+ 901 """
+ 902d=self.getcomponentsdetails(product,force_refresh)
+ 903returnd[component]
+ 904
+ 905defgetcomponents(self,product,force_refresh=False):
+ 906"""
+ 907 Return a list of component names for the passed product.
+ 908
+ 909 On first invocation the value is cached, and subsequent calls
+ 910 will return the cached data.
+ 911
+ 912 :param force_refresh: Force refreshing the cache, and return
+ 913 the new data
+ 914 """
+ 915proddict=self._lookup_product_in_cache(product)
+ 916product_id=proddict.get("id",None)
+ 917
+ 918if(force_refreshorproduct_idisNoneor
+ 919"components"notinproddict):
+ 920self.refresh_products(
+ 921names=[product],
+ 922include_fields=["name","id","components.name"])
+ 923proddict=self._lookup_product_in_cache(product)
+ 924if"id"notinproddict:
+ 925raiseBugzillaError("Product '%s' not found"%product)
+ 926product_id=proddict["id"]
+ 927
+ 928ifproduct_idnotinself._cache.component_names:
+ 929names=[]
+ 930forcompinproddict.get("components",[]):
+ 931name=comp.get("name")
+ 932ifname:
+ 933names.append(name)
+ 934self._cache.component_names[product_id]=names
+ 935
+ 936returnself._cache.component_names[product_id]
+ 937
+ 938
+ 939############################
+ 940# component adding/editing #
+ 941############################
+ 942
+ 943def_component_data_convert(self,data,update=False):
+ 944# Back compat for the old RH interface
+ 945convert_fields=[
+ 946("initialowner","default_assignee"),
+ 947("initialqacontact","default_qa_contact"),
+ 948("initialcclist","default_cc"),
+ 949]
+ 950forold,newinconvert_fields:
+ 951ifoldindata:
+ 952data[new]=data.pop(old)
+ 953
+ 954ifupdate:
+ 955names={"product":data.pop("product"),
+ 956"component":data.pop("component")}
+ 957updates={}
+ 958forkinlist(data.keys()):
+ 959updates[k]=data.pop(k)
+ 960
+ 961data["names"]=[names]
+ 962data["updates"]=updates
+ 963
+ 964
+ 965defaddcomponent(self,data):
+ 966"""
+ 967 A method to create a component in Bugzilla. Takes a dict, with the
+ 968 following elements:
+ 969
+ 970 product: The product to create the component in
+ 971 component: The name of the component to create
+ 972 description: A one sentence summary of the component
+ 973 default_assignee: The bugzilla login (email address) of the initial
+ 974 owner of the component
+ 975 default_qa_contact (optional): The bugzilla login of the
+ 976 initial QA contact
+ 977 default_cc: (optional) The initial list of users to be CC'ed on
+ 978 new bugs for the component.
+ 979 is_active: (optional) If False, the component is hidden from
+ 980 the component list when filing new bugs.
+ 981 """
+ 982data=data.copy()
+ 983self._component_data_convert(data)
+ 984returnself._backend.component_create(data)
+ 985
+ 986defeditcomponent(self,data):
+ 987"""
+ 988 A method to edit a component in Bugzilla. Takes a dict, with
+ 989 mandatory elements of product. component, and initialowner.
+ 990 All other elements are optional and use the same names as the
+ 991 addcomponent() method.
+ 992 """
+ 993data=data.copy()
+ 994self._component_data_convert(data,update=True)
+ 995returnself._backend.component_update(data)
+ 996
+ 997
+ 998###################
+ 999# getbug* methods #
+1000###################
+1001
+1002def_process_include_fields(self,include_fields,exclude_fields,
+1003extra_fields):
+1004"""
+1005 Internal helper to process include_fields lists
+1006 """
+1007def_convert_fields(_in):
+1008fornewname,oldnameinself._get_api_aliases():
+1009ifoldnamein_in:
+1010_in.remove(oldname)
+1011ifnewnamenotin_in:
+1012_in.append(newname)
+1013return_in
+1014
+1015ret={}
+1016ifinclude_fields:
+1017include_fields=_convert_fields(include_fields)
+1018if"id"notininclude_fields:
+1019include_fields.append("id")
+1020ret["include_fields"]=include_fields
+1021ifexclude_fields:
+1022exclude_fields=_convert_fields(exclude_fields)
+1023ret["exclude_fields"]=exclude_fields
+1024ifself._supports_getbug_extra_fields():
+1025ifextra_fields:
+1026ret["extra_fields"]=_convert_fields(extra_fields)
+1027returnret
+1028
+1029def_get_bug_autorefresh(self):
+1030"""
+1031 This value is passed to Bug.autorefresh for all fetched bugs.
+1032 If True, and an uncached attribute is requested from a Bug,
+1033 the Bug will update its contents and try again.
+1034 """
+1035returnself._bug_autorefresh
+1036
+1037def_set_bug_autorefresh(self,val):
+1038self._bug_autorefresh=bool(val)
+1039bug_autorefresh=property(_get_bug_autorefresh,_set_bug_autorefresh)
+1040
+1041
+1042def_getbug_extra_fields(self):
+1043"""
+1044 Extra fields that need to be explicitly
+1045 requested from Bug.get in order for the data to be returned.
+1046 """
+1047rhbz_extra_fields=[
+1048"comments","description",
+1049"external_bugs","flags","sub_components",
+1050"tags",
+1051]
+1052ifself._is_redhat_bugzilla:
+1053returnrhbz_extra_fields
+1054return[]
+1055
+1056def_supports_getbug_extra_fields(self):
+1057"""
+1058 Return True if the bugzilla instance supports passing
+1059 extra_fields to getbug
+1060
+1061 As of Dec 2012 it seems like only RH bugzilla actually has behavior
+1062 like this, for upstream bz it returns all info for every Bug.get()
+1063 """
+1064returnself._is_redhat_bugzilla
+1065
+1066
+1067def_getbugs(self,idlist,permissive,
+1068include_fields=None,exclude_fields=None,extra_fields=None):
+1069"""
+1070 Return a list of dicts of full bug info for each given bug id.
+1071 bug ids that couldn't be found will return None instead of a dict.
+1072 """
+1073ids=[]
+1074aliases=[]
+1075
+1076def_alias_or_int(_v):
+1077ifstr(_v).isdigit():
+1078returnint(_v),None
+1079returnNone,str(_v)
+1080
+1081foridstrinidlist:
+1082idint,alias=_alias_or_int(idstr)
+1083ifalias:
+1084aliases.append(alias)
+1085else:
+1086ids.append(idstr)
+1087
+1088if(include_fieldsisnotNoneandaliases
+1089and"alias"notininclude_fields):
+1090# Extra field to prevent sorting (see below) from causing an error
+1091include_fields.append("alias")
+1092
+1093extra_fields=listify(extra_fieldsor[])
+1094extra_fields+=self._getbug_extra_fields()
+1095
+1096getbugdata={}
+1097ifpermissive:
+1098getbugdata["permissive"]=1
+1099
+1100getbugdata.update(self._process_include_fields(
+1101include_fields,exclude_fields,extra_fields))
+1102
+1103r=self._backend.bug_get(ids,aliases,getbugdata)
+1104
+1105# Do some wrangling to ensure we return bugs in the same order
+1106# the were passed in, for historical reasons
+1107ret=[]
+1108foridvalinidlist:
+1109idint,alias=_alias_or_int(idval)
+1110forbugdictinr["bugs"]:
+1111ifidintisnotNoneandidint!=bugdict.get("id",None):
+1112continue
+1113aliaslist=listify(bugdict.get("alias",None)or[])
+1114ifaliasandaliasnotinaliaslist:
+1115continue
+1116
+1117ret.append(bugdict)
+1118break
+1119returnret
+1120
+1121def_getbug(self,objid,**kwargs):
+1122"""
+1123 Thin wrapper around _getbugs to handle the slight argument tweaks
+1124 for fetching a single bug. The main bit is permissive=False, which
+1125 will tell bugzilla to raise an explicit error if we can't fetch
+1126 that bug.
+1127
+1128 This logic is called from Bug() too
+1129 """
+1130returnself._getbugs([objid],permissive=False,**kwargs)[0]
+1131
+1132defgetbug(self,objid,
+1133include_fields=None,exclude_fields=None,extra_fields=None):
+1134"""
+1135 Return a Bug object with the full complement of bug data
+1136 already loaded.
+1137 """
+1138data=self._getbug(objid,
+1139include_fields=include_fields,exclude_fields=exclude_fields,
+1140extra_fields=extra_fields)
+1141returnBug(self,dict=data,autorefresh=self.bug_autorefresh)
+1142
+1143defgetbugs(self,idlist,
+1144include_fields=None,exclude_fields=None,extra_fields=None,
+1145permissive=True):
+1146"""
+1147 Return a list of Bug objects with the full complement of bug data
+1148 already loaded. If there's a problem getting the data for a given id,
+1149 the corresponding item in the returned list will be None.
+1150 """
+1151data=self._getbugs(idlist,include_fields=include_fields,
+1152exclude_fields=exclude_fields,extra_fields=extra_fields,
+1153permissive=permissive)
+1154return[(bandBug(self,dict=b,
+1155autorefresh=self.bug_autorefresh))orNone
+1156forbindata]
+1157
+1158defget_comments(self,idlist):
+1159"""
+1160 Returns a dictionary of bugs and comments. The comments key will
+1161 be empty. See bugzilla docs for details
+1162 """
+1163returnself._backend.bug_comments(idlist,{})
+1164
+1165
+1166#################
+1167# query methods #
+1168#################
+1169
+1170defbuild_query(self,
+1171product=None,
+1172component=None,
+1173version=None,
+1174long_desc=None,
+1175bug_id=None,
+1176short_desc=None,
+1177cc=None,
+1178assigned_to=None,
+1179reporter=None,
+1180qa_contact=None,
+1181status=None,
+1182blocked=None,
+1183dependson=None,
+1184keywords=None,
+1185keywords_type=None,
+1186url=None,
+1187url_type=None,
+1188status_whiteboard=None,
+1189status_whiteboard_type=None,
+1190fixed_in=None,
+1191fixed_in_type=None,
+1192flag=None,
+1193alias=None,
+1194qa_whiteboard=None,
+1195devel_whiteboard=None,
+1196bug_severity=None,
+1197priority=None,
+1198target_release=None,
+1199target_milestone=None,
+1200emailtype=None,
+1201include_fields=None,
+1202quicksearch=None,
+1203savedsearch=None,
+1204savedsearch_sharer_id=None,
+1205sub_component=None,
+1206tags=None,
+1207exclude_fields=None,
+1208extra_fields=None,
+1209limit=None,
+1210resolution=None):
+1211"""
+1212 Build a query string from passed arguments. Will handle
+1213 query parameter differences between various bugzilla versions.
+1214
+1215 Most of the parameters should be self-explanatory. However,
+1216 if you want to perform a complex query, and easy way is to
+1217 create it with the bugzilla web UI, copy the entire URL it
+1218 generates, and pass it to the static method
+1219
+1220 Bugzilla.url_to_query
+1221
+1222 Then pass the output to Bugzilla.query()
+1223
+1224 For details about the specific argument formats, see the bugzilla docs:
+1225 https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#search-bugs
+1226 """
+1227query={
+1228"alias":alias,
+1229"product":listify(product),
+1230"component":listify(component),
+1231"version":version,
+1232"id":bug_id,
+1233"short_desc":short_desc,
+1234"bug_status":status,
+1235"bug_severity":bug_severity,
+1236"priority":priority,
+1237"target_release":target_release,
+1238"target_milestone":target_milestone,
+1239"tag":listify(tags),
+1240"quicksearch":quicksearch,
+1241"savedsearch":savedsearch,
+1242"sharer_id":savedsearch_sharer_id,
+1243"limit":limit,
+1244"resolution":resolution,
+1245
+1246# RH extensions... don't add any more. See comment below
+1247"sub_components":listify(sub_component),
+1248}
+1249
+1250defadd_bool(bzkey,value,bool_id,booltype=None):
+1251value=listify(value)
+1252ifvalueisNone:
+1253returnbool_id
+1254
+1255query["query_format"]="advanced"
+1256forboolvalinvalue:
+1257defmake_bool_str(prefix):
+1258# pylint: disable=cell-var-from-loop
+1259return"%s%i-0-0"%(prefix,bool_id)
+1260
+1261query[make_bool_str("field")]=bzkey
+1262query[make_bool_str("value")]=boolval
+1263query[make_bool_str("type")]=booltypeor"substring"
+1264
+1265bool_id+=1
+1266returnbool_id
+1267
+1268# RH extensions that we have to maintain here for back compat,
+1269# but all future custom fields should be specified via
+1270# cli --field option, or via extending the query dict() manually.
+1271# No more supporting custom fields in this API
+1272bool_id=0
+1273bool_id=add_bool("keywords",keywords,bool_id,keywords_type)
+1274bool_id=add_bool("blocked",blocked,bool_id)
+1275bool_id=add_bool("dependson",dependson,bool_id)
+1276bool_id=add_bool("bug_file_loc",url,bool_id,url_type)
+1277bool_id=add_bool("cf_fixed_in",fixed_in,bool_id,fixed_in_type)
+1278bool_id=add_bool("flagtypes.name",flag,bool_id)
+1279bool_id=add_bool("status_whiteboard",
+1280status_whiteboard,bool_id,status_whiteboard_type)
+1281bool_id=add_bool("cf_qa_whiteboard",qa_whiteboard,bool_id)
+1282bool_id=add_bool("cf_devel_whiteboard",devel_whiteboard,bool_id)
+1283
+1284defadd_email(key,value,count):
+1285ifvalueisNone:
+1286returncount
+1287ifnotemailtype:
+1288query[key]=value
+1289returncount
+1290
+1291query["query_format"]="advanced"
+1292query['email%i'%count]=value
+1293query['email%s%i'%(key,count)]=True
+1294query['emailtype%i'%count]=emailtype
+1295returncount+1
+1296
+1297email_count=1
+1298email_count=add_email("cc",cc,email_count)
+1299email_count=add_email("assigned_to",assigned_to,email_count)
+1300email_count=add_email("reporter",reporter,email_count)
+1301email_count=add_email("qa_contact",qa_contact,email_count)
+1302
+1303iflong_descisnotNone:
+1304query["query_format"]="advanced"
+1305query["longdesc"]=long_desc
+1306query["longdesc_type"]="allwordssubstr"
+1307
+1308# 'include_fields' only available for Bugzilla4+
+1309# 'extra_fields' is an RHBZ extension
+1310query.update(self._process_include_fields(
+1311include_fields,exclude_fields,extra_fields))
+1312
+1313# Strip out None elements in the dict
+1314fork,vinquery.copy().items():
+1315ifvisNone:
+1316delquery[k]
+1317
+1318self.pre_translation(query)
+1319returnquery
+1320
+1321
+1322defquery_return_extra(self,query):
+1323"""
+1324 Same as `query()`, but the return value is altered to be
+1325 (buglist, values), where `values` is raw dictionary output from
+1326 the API call, excluding the bug content. For example this may
+1327 include a `limit` value if the bugzilla instance puts an implied
+1328 limit on returned result numbers.
+1329 """
+1330try:
+1331r=self._backend.bug_search(query)
+1332log.debug("bug_search returned:\n%s",str(r))
+1333exceptExceptionase:
+1334# Try to give a hint in the error message if url_to_query
+1335# isn't supported by this bugzilla instance
+1336if("query_format"notinstr(e)or
+1337notBugzillaError.get_bugzilla_error_code(e)or
+1338self._get_version()>=5.0):
+1339raise
+1340raiseBugzillaError("%s\nYour bugzilla instance does not "
+1341"appear to support API queries derived from bugzilla "
+1342"web URL queries."%e)fromNone
+1343
+1344rawbugs=r.pop("bugs")
+1345log.debug("Query returned %s bugs",len(rawbugs))
+1346bugs=[Bug(self,dict=b,
+1347autorefresh=self.bug_autorefresh)forbinrawbugs]
+1348
+1349returnbugs,r
+1350
+1351defquery(self,query):
+1352"""
+1353 Pass search terms to bugzilla and and return a list of matching
+1354 Bug objects.
+1355
+1356 See `build_query` for more details about constructing the
+1357 `query` dict parameter.
+1358 """
+1359bugs,dummy=self.query_return_extra(query)
+1360returnbugs
+1361
+1362defpre_translation(self,query):
+1363"""
+1364 In order to keep the API the same, Bugzilla4 needs to process the
+1365 query and the result. This also applies to the refresh() function
+1366 """
+1367ifself._is_redhat_bugzilla:
+1368_RHBugzillaConverters.pre_translation(query)
+1369query.update(self._process_include_fields(
+1370query.get("include_fields",[]),None,None))
+1371
+1372defpost_translation(self,query,bug):
+1373"""
+1374 In order to keep the API the same, Bugzilla4 needs to process the
+1375 query and the result. This also applies to the refresh() function
+1376 """
+1377ifself._is_redhat_bugzilla:
+1378_RHBugzillaConverters.post_translation(query,bug)
+1379
+1380defbugs_history_raw(self,bug_ids):
+1381"""
+1382 Experimental. Gets the history of changes for
+1383 particular bugs in the database.
+1384 """
+1385returnself._backend.bug_history(bug_ids,{})
+1386
+1387
+1388#######################################
+1389# Methods for modifying existing bugs #
+1390#######################################
+1391
+1392# Bug() also has individual methods for many ops, like setassignee()
+1393
+1394defupdate_bugs(self,ids,updates):
+1395"""
+1396 A thin wrapper around bugzilla Bug.update(). Used to update all
+1397 values of an existing bug report, as well as add comments.
+1398
+1399 The dictionary passed to this function should be generated with
+1400 build_update(), otherwise we cannot guarantee back compatibility.
+1401 """
+1402tmp=updates.copy()
+1403returnself._backend.bug_update(listify(ids),tmp)
+1404
+1405defupdate_tags(self,idlist,tags_add=None,tags_remove=None):
+1406"""
+1407 Updates the 'tags' field for a bug.
+1408 """
+1409tags={}
+1410iftags_add:
+1411tags["add"]=listify(tags_add)
+1412iftags_remove:
+1413tags["remove"]=listify(tags_remove)
+1414
+1415d={
+1416"tags":tags,
+1417}
+1418
+1419returnself._backend.bug_update_tags(listify(idlist),d)
+1420
+1421defupdate_flags(self,idlist,flags):
+1422"""
+1423 A thin back compat wrapper around build_update(flags=X)
+1424 """
+1425returnself.update_bugs(idlist,self.build_update(flags=flags))
+1426
+1427
+1428defbuild_update(self,
+1429alias=None,
+1430assigned_to=None,
+1431blocks_add=None,
+1432blocks_remove=None,
+1433blocks_set=None,
+1434depends_on_add=None,
+1435depends_on_remove=None,
+1436depends_on_set=None,
+1437cc_add=None,
+1438cc_remove=None,
+1439is_cc_accessible=None,
+1440comment=None,
+1441comment_private=None,
+1442component=None,
+1443deadline=None,
+1444dupe_of=None,
+1445estimated_time=None,
+1446groups_add=None,
+1447groups_remove=None,
+1448keywords_add=None,
+1449keywords_remove=None,
+1450keywords_set=None,
+1451op_sys=None,
+1452platform=None,
+1453priority=None,
+1454product=None,
+1455qa_contact=None,
+1456is_creator_accessible=None,
+1457remaining_time=None,
+1458reset_assigned_to=None,
+1459reset_qa_contact=None,
+1460resolution=None,
+1461see_also_add=None,
+1462see_also_remove=None,
+1463severity=None,
+1464status=None,
+1465summary=None,
+1466target_milestone=None,
+1467target_release=None,
+1468url=None,
+1469version=None,
+1470whiteboard=None,
+1471work_time=None,
+1472fixed_in=None,
+1473qa_whiteboard=None,
+1474devel_whiteboard=None,
+1475internal_whiteboard=None,
+1476sub_component=None,
+1477flags=None,
+1478comment_tags=None,
+1479minor_update=None):
+1480"""
+1481 Returns a python dict() with properly formatted parameters to
+1482 pass to update_bugs(). See bugzilla documentation for the format
+1483 of the individual fields:
+1484
+1485 https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#create-bug
+1486 """
+1487ret={}
+1488rhbzret={}
+1489
+1490# These are only supported for rhbugzilla
+1491#
+1492# This should not be extended any more.
+1493# If people want to handle custom fields, manually extend the
+1494# returned dictionary.
+1495rhbzargs={
+1496"fixed_in":fixed_in,
+1497"devel_whiteboard":devel_whiteboard,
+1498"qa_whiteboard":qa_whiteboard,
+1499"internal_whiteboard":internal_whiteboard,
+1500"sub_component":sub_component,
+1501}
+1502ifself._is_redhat_bugzilla:
+1503rhbzret=_RHBugzillaConverters.convert_build_update(
+1504component=component,**rhbzargs)
+1505else:
+1506forkey,valinrhbzargs.items():
+1507ifvalisnotNone:
+1508raiseValueError("bugzilla instance does not support "
+1509"updating '%s'"%key)
+1510
+1511defs(key,val,convert=None):
+1512ifvalisNone:
+1513return
+1514ifconvert:
+1515val=convert(val)
+1516ret[key]=val
+1517
+1518defadd_dict(key,add,remove,_set=None):
+1519ifaddisremoveis_setisNone:
+1520return
+1521
+1522newdict={}
+1523ifaddisnotNone:
+1524newdict["add"]=listify(add)
+1525ifremoveisnotNone:
+1526newdict["remove"]=listify(remove)
+1527if_setisnotNone:
+1528newdict["set"]=listify(_set)
+1529ret[key]=newdict
+1530
+1531
+1532s("alias",alias)
+1533s("assigned_to",assigned_to)
+1534s("is_cc_accessible",is_cc_accessible,bool)
+1535s("component",component)
+1536s("deadline",deadline)
+1537s("dupe_of",dupe_of,int)
+1538s("estimated_time",estimated_time,int)
+1539s("op_sys",op_sys)
+1540s("platform",platform)
+1541s("priority",priority)
+1542s("product",product)
+1543s("qa_contact",qa_contact)
+1544s("is_creator_accessible",is_creator_accessible,bool)
+1545s("remaining_time",remaining_time,float)
+1546s("reset_assigned_to",reset_assigned_to,bool)
+1547s("reset_qa_contact",reset_qa_contact,bool)
+1548s("resolution",resolution)
+1549s("severity",severity)
+1550s("status",status)
+1551s("summary",summary)
+1552s("target_milestone",target_milestone)
+1553s("target_release",target_release)
+1554s("url",url)
+1555s("version",version)
+1556s("whiteboard",whiteboard)
+1557s("work_time",work_time,float)
+1558s("flags",flags)
+1559s("comment_tags",comment_tags,listify)
+1560s("minor_update",minor_update,bool)
+1561
+1562add_dict("blocks",blocks_add,blocks_remove,blocks_set)
+1563add_dict("depends_on",depends_on_add,depends_on_remove,depends_on_set)
+1564add_dict("cc",cc_add,cc_remove)
+1565add_dict("groups",groups_add,groups_remove)
+1566add_dict("keywords",keywords_add,keywords_remove,keywords_set)
+1567add_dict("see_also",see_also_add,see_also_remove)
+1568
+1569ifcommentisnotNone:
+1570ret["comment"]={"comment":comment}
+1571ifcomment_private:
+1572ret["comment"]["is_private"]=comment_private
+1573
+1574ret.update(rhbzret)
+1575returnret
+1576
+1577
+1578########################################
+1579# Methods for working with attachments #
+1580########################################
+1581
+1582defattachfile(self,idlist,attachfile,description,**kwargs):
+1583"""
+1584 Attach a file to the given bug IDs. Returns the ID of the attachment
+1585 or raises XMLRPC Fault if something goes wrong.
+1586
+1587 attachfile may be a filename (which will be opened) or a file-like
+1588 object, which must provide a 'read' method. If it's not one of these,
+1589 this method will raise a TypeError.
+1590 description is the short description of this attachment.
+1591
+1592 Optional keyword args are as follows:
+1593 file_name: this will be used as the filename for the attachment.
+1594 REQUIRED if attachfile is a file-like object with no
+1595 'name' attribute, otherwise the filename or .name
+1596 attribute will be used.
+1597 comment: An optional comment about this attachment.
+1598 is_private: Set to True if the attachment should be marked private.
+1599 is_patch: Set to True if the attachment is a patch.
+1600 content_type: The mime-type of the attached file. Defaults to
+1601 application/octet-stream if not set. NOTE that text
+1602 files will *not* be viewable in bugzilla unless you
+1603 remember to set this to text/plain. So remember that!
+1604
+1605 Returns the list of attachment ids that were added. If only one
+1606 attachment was added, we return the single int ID for back compat
+1607 """
+1608ifisinstance(attachfile,str):
+1609f=open(attachfile,"rb")
+1610elifhasattr(attachfile,'read'):
+1611f=attachfile
+1612else:
+1613raiseTypeError("attachfile must be filename or file-like object")
+1614
+1615# Back compat
+1616if"contenttype"inkwargs:
+1617kwargs["content_type"]=kwargs.pop("contenttype")
+1618if"ispatch"inkwargs:
+1619kwargs["is_patch"]=kwargs.pop("ispatch")
+1620if"isprivate"inkwargs:
+1621kwargs["is_private"]=kwargs.pop("isprivate")
+1622if"filename"inkwargs:
+1623kwargs["file_name"]=kwargs.pop("filename")
+1624
+1625kwargs['summary']=description
+1626
+1627data=f.read()
+1628ifnotisinstance(data,bytes):# pragma: no cover
+1629data=data.encode(locale.getpreferredencoding())
+1630
+1631if'file_name'notinkwargsandhasattr(f,"name"):
+1632kwargs['file_name']=os.path.basename(f.name)
+1633if'content_type'notinkwargs:
+1634ctype=None
+1635ifkwargs['file_name']:
+1636ctype=mimetypes.guess_type(
+1637kwargs['file_name'],strict=False)[0]
+1638kwargs['content_type']=ctypeor'application/octet-stream'
+1639
+1640ret=self._backend.bug_attachment_create(
+1641listify(idlist),data,kwargs)
+1642
+1643if"attachments"inret:
+1644# Up to BZ 4.2
+1645ret=[int(k)forkinret["attachments"].keys()]
+1646elif"ids"inret:
+1647# BZ 4.4+
+1648ret=ret["ids"]
+1649
+1650ifisinstance(ret,list)andlen(ret)==1:
+1651ret=ret[0]
+1652returnret
+1653
+1654defopenattachment_data(self,attachment_dict):
+1655"""
+1656 Helper for turning passed API attachment dictionary into a
+1657 filelike object
+1658 """
+1659ret=BytesIO()
+1660data=attachment_dict["data"]
+1661
+1662ifhasattr(data,"data"):
+1663# This is for xmlrpc Binary
+1664content=data.data# pragma: no cover
+1665else:
+1666importbase64
+1667content=base64.b64decode(data)
+1668
+1669ret.write(content)
+1670ret.name=attachment_dict["file_name"]
+1671ret.seek(0)
+1672returnret
+1673
+1674defopenattachment(self,attachid):
+1675"""
+1676 Get the contents of the attachment with the given attachment ID.
+1677 Returns a file-like object.
+1678 """
+1679attachments=self.get_attachments(None,attachid)
+1680data=attachments["attachments"][str(attachid)]
+1681returnself.openattachment_data(data)
+1682
+1683defupdateattachmentflags(self,bugid,attachid,flagname,**kwargs):
+1684"""
+1685 Updates a flag for the given attachment ID.
+1686 Optional keyword args are:
+1687 status: new status for the flag ('-', '+', '?', 'X')
+1688 requestee: new requestee for the flag
+1689 """
+1690# Bug ID was used for the original custom redhat API, no longer
+1691# needed though
+1692ignore=bugid
+1693
+1694flags={"name":flagname}
+1695flags.update(kwargs)
+1696attachment_ids=[int(attachid)]
+1697update={'flags':[flags]}
+1698
+1699returnself._backend.bug_attachment_update(attachment_ids,update)
+1700
+1701defget_attachments(self,ids,attachment_ids,
+1702include_fields=None,exclude_fields=None):
+1703"""
+1704 Wrapper for Bug.attachments. One of ids or attachment_ids is required
+1705
+1706 :param ids: Get attachments for this bug ID
+1707 :param attachment_ids: Specific attachment ID to get
+1708
+1709 https://bugzilla.readthedocs.io/en/latest/api/core/v1/attachment.html#get-attachment
+1710 """
+1711params={}
+1712ifinclude_fields:
+1713params["include_fields"]=listify(include_fields)
+1714ifexclude_fields:
+1715params["exclude_fields"]=listify(exclude_fields)
+1716
+1717ifattachment_ids:
+1718returnself._backend.bug_attachment_get(attachment_ids,params)
+1719returnself._backend.bug_attachment_get_all(ids,params)
+1720
+1721
+1722#####################
+1723# createbug methods #
+1724#####################
+1725
+1726createbug_required=('product','component','summary','version',
+1727'description')
+1728
+1729defbuild_createbug(self,
+1730product=None,
+1731component=None,
+1732version=None,
+1733summary=None,
+1734description=None,
+1735comment_private=None,
+1736blocks=None,
+1737cc=None,
+1738assigned_to=None,
+1739keywords=None,
+1740depends_on=None,
+1741groups=None,
+1742op_sys=None,
+1743platform=None,
+1744priority=None,
+1745qa_contact=None,
+1746resolution=None,
+1747severity=None,
+1748status=None,
+1749target_milestone=None,
+1750target_release=None,
+1751url=None,
+1752sub_component=None,
+1753alias=None,
+1754comment_tags=None):
+1755"""
+1756 Returns a python dict() with properly formatted parameters to
+1757 pass to createbug(). See bugzilla documentation for the format
+1758 of the individual fields:
+1759
+1760 https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#update-bug
+1761 """
+1762
+1763localdict={}
+1764ifblocks:
+1765localdict["blocks"]=listify(blocks)
+1766ifcc:
+1767localdict["cc"]=listify(cc)
+1768ifdepends_on:
+1769localdict["depends_on"]=listify(depends_on)
+1770ifgroupsisnotNone:
+1771localdict["groups"]=listify(groups)
+1772ifkeywords:
+1773localdict["keywords"]=listify(keywords)
+1774ifdescription:
+1775localdict["description"]=description
+1776ifcomment_private:
+1777localdict["comment_is_private"]=True
+1778
+1779# Most of the machinery and formatting here is the same as
+1780# build_update, so reuse that as much as possible
+1781ret=self.build_update(product=product,component=component,
+1782version=version,summary=summary,op_sys=op_sys,
+1783platform=platform,priority=priority,qa_contact=qa_contact,
+1784resolution=resolution,severity=severity,status=status,
+1785target_milestone=target_milestone,
+1786target_release=target_release,url=url,
+1787assigned_to=assigned_to,sub_component=sub_component,
+1788alias=alias,comment_tags=comment_tags)
+1789
+1790ret.update(localdict)
+1791returnret
+1792
+1793def_validate_createbug(self,*args,**kwargs):
+1794# Previous API required users specifying keyword args that mapped
+1795# to the XMLRPC arg names. Maintain that bad compat, but also allow
+1796# receiving a single dictionary like query() does
+1797ifkwargsandargs:# pragma: no cover
+1798raiseBugzillaError("createbug: cannot specify positional "
+1799"args=%s with kwargs=%s, must be one or the "
+1800"other."%(args,kwargs))
+1801ifargs:
+1802iflen(args)>1ornotisinstance(args[0],dict):
+1803raiseBugzillaError(# pragma: no cover
+1804"createbug: positional arguments only "
+1805"accept a single dictionary.")
+1806data=args[0]
+1807else:
+1808data=kwargs
+1809
+1810# If we're getting a call that uses an old fieldname, convert it to the
+1811# new fieldname instead.
+1812fornewname,oldnameinself._get_api_aliases():
+1813if(newnameinself.createbug_requiredand
+1814newnamenotindataand
+1815oldnameindata):
+1816data[newname]=data.pop(oldname)
+1817
+1818# Back compat handling for check_args
+1819if"check_args"indata:
+1820deldata["check_args"]
+1821
+1822returndata
+1823
+1824defcreatebug(self,*args,**kwargs):
+1825"""
+1826 Create a bug with the given info. Returns a new Bug object.
+1827 Check bugzilla API documentation for valid values, at least
+1828 product, component, summary, version, and description need to
+1829 be passed.
+1830 """
+1831data=self._validate_createbug(*args,**kwargs)
+1832rawbug=self._backend.bug_create(data)
+1833returnBug(self,bug_id=rawbug["id"],
+1834autorefresh=self.bug_autorefresh)
+1835
+1836
+1837##############################
+1838# Methods for handling Users #
+1839##############################
+1840
+1841defgetuser(self,username):
+1842"""
+1843 Return a bugzilla User for the given username
+1844
+1845 :arg username: The username used in bugzilla.
+1846 :raises XMLRPC Fault: Code 51 if the username does not exist
+1847 :returns: User record for the username
+1848 """
+1849ret=self.getusers(username)
+1850returnretandret[0]
+1851
+1852defgetusers(self,userlist):
+1853"""
+1854 Return a list of Users from .
+1855
+1856 :userlist: List of usernames to lookup
+1857 :returns: List of User records
+1858 """
+1859userlist=listify(userlist)
+1860rawusers=self._backend.user_get({"names":userlist})
+1861userobjs=[User(self,**rawuser)forrawuserin
+1862rawusers.get('users',[])]
+1863
+1864# Return users in same order they were passed in
+1865ret=[]
+1866foruinuserlist:
+1867foruobjinuserobjs[:]:
+1868ifuobj.email==u:
+1869userobjs.remove(uobj)
+1870ret.append(uobj)
+1871break
+1872ret+=userobjs
+1873returnret
+1874
+1875
+1876defsearchusers(self,pattern):
+1877"""
+1878 Return a bugzilla User for the given list of patterns
+1879
+1880 :arg pattern: List of patterns to match against.
+1881 :returns: List of User records
+1882 """
+1883rawusers=self._backend.user_get({"match":listify(pattern)})
+1884return[User(self,**rawuser)forrawuserin
+1885rawusers.get('users',[])]
+1886
+1887defcreateuser(self,email,name='',password=''):
+1888"""
+1889 Return a bugzilla User for the given username
+1890
+1891 :arg email: The email address to use in bugzilla
+1892 :kwarg name: Real name to associate with the account
+1893 :kwarg password: Password to set for the bugzilla account
+1894 :raises XMLRPC Fault: Code 501 if the username already exists
+1895 Code 500 if the email address isn't valid
+1896 Code 502 if the password is too short
+1897 Code 503 if the password is too long
+1898 :return: User record for the username
+1899 """
+1900args={"email":email}
+1901ifname:
+1902args["name"]=name
+1903ifpassword:
+1904args["password"]=password
+1905self._backend.user_create(args)
+1906returnself.getuser(email)
+1907
+1908defupdateperms(self,user,action,groups):
+1909"""
+1910 A method to update the permissions (group membership) of a bugzilla
+1911 user.
+1912
+1913 :arg user: The e-mail address of the user to be acted upon. Can
+1914 also be a list of emails.
+1915 :arg action: add, remove, or set
+1916 :arg groups: list of groups to be added to (i.e. ['fedora_contrib'])
+1917 """
+1918groups=listify(groups)
+1919ifaction=="rem":
+1920action="remove"
+1921ifactionnotin["add","remove","set"]:
+1922raiseBugzillaError("Unknown user permission action '%s'"%action)
+1923
+1924update={
+1925"names":listify(user),
+1926"groups":{
+1927action:groups,
+1928}
+1929}
+1930
+1931returnself._backend.user_update(update)
+1932
+1933
+1934###############################
+1935# Methods for handling Groups #
+1936###############################
+1937
+1938def_getgroups(self,names,membership=False):
+1939"""
+1940 Return a list of groups that match criteria.
+1941
+1942 :kwarg ids: list of group ids to return data on
+1943 :kwarg membership: boolean specifying wether to query the members
+1944 of the group or not.
+1945 :raises XMLRPC Fault: Code 51: if a Bad Login Name was sent to the
+1946 names array.
+1947 Code 304: if the user was not authorized to see user they
+1948 requested.
+1949 Code 505: user is logged out and can't use the match or ids
+1950 parameter.
+1951 Code 805: logged in user do not have enough priviledges to view
+1952 groups.
+1953 """
+1954params={"membership":membership}
+1955params['names']=listify(names)
+1956returnself._backend.group_get(params)
+1957
+1958defgetgroup(self,name,membership=False):
+1959"""
+1960 Return a bugzilla Group for the given name
+1961
+1962 :arg name: The group name used in bugzilla.
+1963 :raises XMLRPC Fault: Code 51 if the name does not exist
+1964 :raises XMLRPC Fault: Code 805 if the user does not have enough
+1965 permissions to view groups
+1966 :returns: Group record for the name
+1967 """
+1968ret=self.getgroups(name,membership=membership)
+1969returnretandret[0]
+1970
+1971defgetgroups(self,grouplist,membership=False):
+1972"""
+1973 Return a list of Groups from .
+1974
+1975 :userlist: List of group names to lookup
+1976 :returns: List of Group records
+1977 """
+1978grouplist=listify(grouplist)
+1979groupobjs=[
+1980Group(self,**rawgroup)
+1981forrawgroupinself._getgroups(
+1982names=grouplist,membership=membership).get('groups',[])
+1983]
+1984
+1985# Return in same order they were passed in
+1986ret=[]
+1987forgingrouplist:
+1988forgobjingroupobjs[:]:
+1989ifgobj.name==g:
+1990groupobjs.remove(gobj)
+1991ret.append(gobj)
+1992break
+1993ret+=groupobjs
+1994returnret
+1995
+1996
+1997#############################
+1998# ExternalBugs API wrappers #
+1999#############################
+2000
+2001defadd_external_tracker(self,bug_ids,ext_bz_bug_id,ext_type_id=None,
+2002ext_type_description=None,ext_type_url=None,
+2003ext_status=None,ext_description=None,
+2004ext_priority=None):
+2005"""
+2006 Wrapper method to allow adding of external tracking bugs using the
+2007 ExternalBugs::WebService::add_external_bug method.
+2008
+2009 This is documented at
+2010 https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#add-external-bug
+2011
+2012 bug_ids: A single bug id or list of bug ids to have external trackers
+2013 added.
+2014 ext_bz_bug_id: The external bug id (ie: the bug number in the
+2015 external tracker).
+2016 ext_type_id: The external tracker id as used by Bugzilla.
+2017 ext_type_description: The external tracker description as used by
+2018 Bugzilla.
+2019 ext_type_url: The external tracker url as used by Bugzilla.
+2020 ext_status: The status of the external bug.
+2021 ext_description: The description of the external bug.
+2022 ext_priority: The priority of the external bug.
+2023 """
+2024param_dict={'ext_bz_bug_id':ext_bz_bug_id}
+2025ifext_type_idisnotNone:
+2026param_dict['ext_type_id']=ext_type_id
+2027ifext_type_descriptionisnotNone:
+2028param_dict['ext_type_description']=ext_type_description
+2029ifext_type_urlisnotNone:
+2030param_dict['ext_type_url']=ext_type_url
+2031ifext_statusisnotNone:
+2032param_dict['ext_status']=ext_status
+2033ifext_descriptionisnotNone:
+2034param_dict['ext_description']=ext_description
+2035ifext_priorityisnotNone:
+2036param_dict['ext_priority']=ext_priority
+2037params={
+2038'bug_ids':listify(bug_ids),
+2039'external_bugs':[param_dict],
+2040}
+2041returnself._backend.externalbugs_add(params)
+2042
+2043defupdate_external_tracker(self,ids=None,ext_type_id=None,
+2044ext_type_description=None,ext_type_url=None,
+2045ext_bz_bug_id=None,bug_ids=None,
+2046ext_status=None,ext_description=None,
+2047ext_priority=None):
+2048"""
+2049 Wrapper method to allow adding of external tracking bugs using the
+2050 ExternalBugs::WebService::update_external_bug method.
+2051
+2052 This is documented at
+2053 https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#update-external-bug
+2054
+2055 ids: A single external tracker bug id or list of external tracker bug
+2056 ids.
+2057 ext_type_id: The external tracker id as used by Bugzilla.
+2058 ext_type_description: The external tracker description as used by
+2059 Bugzilla.
+2060 ext_type_url: The external tracker url as used by Bugzilla.
+2061 ext_bz_bug_id: A single external bug id or list of external bug ids
+2062 (ie: the bug number in the external tracker).
+2063 bug_ids: A single bug id or list of bug ids to have external tracker
+2064 info updated.
+2065 ext_status: The status of the external bug.
+2066 ext_description: The description of the external bug.
+2067 ext_priority: The priority of the external bug.
+2068 """
+2069params={}
+2070ifidsisnotNone:
+2071params['ids']=listify(ids)
+2072ifext_type_idisnotNone:
+2073params['ext_type_id']=ext_type_id
+2074ifext_type_descriptionisnotNone:
+2075params['ext_type_description']=ext_type_description
+2076ifext_type_urlisnotNone:
+2077params['ext_type_url']=ext_type_url
+2078ifext_bz_bug_idisnotNone:
+2079params['ext_bz_bug_id']=listify(ext_bz_bug_id)
+2080ifbug_idsisnotNone:
+2081params['bug_ids']=listify(bug_ids)
+2082ifext_statusisnotNone:
+2083params['ext_status']=ext_status
+2084ifext_descriptionisnotNone:
+2085params['ext_description']=ext_description
+2086ifext_priorityisnotNone:
+2087params['ext_priority']=ext_priority
+2088returnself._backend.externalbugs_update(params)
+2089
+2090defremove_external_tracker(self,ids=None,ext_type_id=None,
+2091ext_type_description=None,ext_type_url=None,
+2092ext_bz_bug_id=None,bug_ids=None):
+2093"""
+2094 Wrapper method to allow removal of external tracking bugs using the
+2095 ExternalBugs::WebService::remove_external_bug method.
+2096
+2097 This is documented at
+2098 https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#remove-external-bug
+2099
+2100 ids: A single external tracker bug id or list of external tracker bug
+2101 ids.
+2102 ext_type_id: The external tracker id as used by Bugzilla.
+2103 ext_type_description: The external tracker description as used by
+2104 Bugzilla.
+2105 ext_type_url: The external tracker url as used by Bugzilla.
+2106 ext_bz_bug_id: A single external bug id or list of external bug ids
+2107 (ie: the bug number in the external tracker).
+2108 bug_ids: A single bug id or list of bug ids to have external tracker
+2109 info updated.
+2110 """
+2111params={}
+2112ifidsisnotNone:
+2113params['ids']=listify(ids)
+2114ifext_type_idisnotNone:
+2115params['ext_type_id']=ext_type_id
+2116ifext_type_descriptionisnotNone:
+2117params['ext_type_description']=ext_type_description
+2118ifext_type_urlisnotNone:
+2119params['ext_type_url']=ext_type_url
+2120ifext_bz_bug_idisnotNone:
+2121params['ext_bz_bug_id']=listify(ext_bz_bug_id)
+2122ifbug_idsisnotNone:
+2123params['bug_ids']=listify(bug_ids)
+2124returnself._backend.externalbugs_remove(params)
+
+
+
+
The main API object. Connects to a bugzilla instance over XMLRPC, and
+provides wrapper functions to simplify dealing with API calls.
+
+
The most common invocation here will just be with just a URL:
+
+
bzapi = Bugzilla("http://bugzilla.example.com")
+
+
+
If you have previously logged into that URL, and have cached login
+tokens, you will automatically be logged in. Otherwise to
+log in, you can either pass auth options to __init__, or call a login
+helper like interactive_login().
+
+
If you are not logged in, you won't be able to access restricted data like
+user email, or perform write actions like bug create/update. But simple
+querys will work correctly.
+
+
If you are unsure if you are logged in, you can check the .logged_in
+property.
+
+
Another way to specify auth credentials is via a 'bugzillarc' file.
+See readconfig() documentation for details.
178def__init__(self,url=-1,user=None,password=None,cookiefile=-1,
+179sslverify=True,tokenfile=-1,use_creds=True,api_key=None,
+180cert=None,configpaths=-1,
+181force_rest=False,force_xmlrpc=False,requests_session=None):
+182"""
+183 :param url: The bugzilla instance URL, which we will connect
+184 to immediately. Most users will want to specify this at
+185 __init__ time, but you can defer connecting by passing
+186 url=None and calling connect(URL) manually
+187 :param user: optional username to connect with
+188 :param password: optional password for the connecting user
+189 :param cert: optional certificate file for client side certificate
+190 authentication
+191 :param cookiefile: Deprecated, raises an error if not -1 or None
+192 :param sslverify: Set this to False to skip SSL hostname and CA
+193 validation checks, like out of date certificate
+194 :param tokenfile: Location to cache the API login token so youi
+195 don't have to keep specifying username/password.
+196 If -1, use the default path. If None, don't use
+197 or save any tokenfile.
+198 :param use_creds: If False, this disables tokenfile
+199 and configpaths by default. This is a convenience option to
+200 unset those values at init time. If those values are later
+201 changed, they may be used for future operations.
+202 :param sslverify: Maps to 'requests' sslverify parameter. Set to
+203 False to disable SSL verification, but it can also be a path
+204 to file or directory for custom certs.
+205 :param api_key: A bugzilla5+ API key
+206 :param configpaths: A list of possible bugzillarc locations.
+207 :param force_rest: Force use of the REST API
+208 :param force_xmlrpc: Force use of the XMLRPC API. If neither force_X
+209 parameter are specified, heuristics will be used to determine
+210 which API to use, with XMLRPC preferred for back compatability.
+211 :param requests_session: An optional requests.Session object the
+212 API will use to contact the remote bugzilla instance. This
+213 way the API user can set up whatever auth bits they may need.
+214 """
+215ifurl==-1:
+216raiseTypeError("Specify a valid bugzilla url, or pass url=None")
+217
+218# Settings the user might want to tweak
+219self.user=useror''
+220self.password=passwordor''
+221self.api_key=api_key
+222self.cert=certorNone
+223self.url=''
+224
+225self._backend=None
+226self._session=None
+227self._user_requests_session=requests_session
+228self._sslverify=sslverify
+229self._cache=_BugzillaAPICache()
+230self._bug_autorefresh=False
+231self._is_redhat_bugzilla=False
+232
+233self._rcfile=_BugzillaRCFile()
+234self._tokencache=_BugzillaTokenCache()
+235
+236self._force_rest=force_rest
+237self._force_xmlrpc=force_xmlrpc
+238
+239ifcookiefilenotin[None,-1]:
+240raiseTypeError("cookiefile is deprecated, don't pass any value.")
+241
+242ifnotuse_creds:
+243tokenfile=None
+244configpaths=[]
+245
+246iftokenfile==-1:
+247tokenfile=self._tokencache.get_default_path()
+248ifconfigpaths==-1:
+249configpaths=_BugzillaRCFile.get_default_configpaths()
+250
+251self._settokenfile(tokenfile)
+252self._setconfigpath(configpaths)
+253
+254ifurl:
+255self.connect(url)
+
+
+
+
Parameters
+
+
+
url: The bugzilla instance URL, which we will connect
+to immediately. Most users will want to specify this at
+__init__ time, but you can defer connecting by passing
+url=None and calling connect(URL) manually
+
user: optional username to connect with
+
password: optional password for the connecting user
+
cert: optional certificate file for client side certificate
+authentication
+
cookiefile: Deprecated, raises an error if not -1 or None
+
sslverify: Set this to False to skip SSL hostname and CA
+validation checks, like out of date certificate
+
tokenfile: Location to cache the API login token so youi
+don't have to keep specifying username/password.
+If -1, use the default path. If None, don't use
+or save any tokenfile.
+
use_creds: If False, this disables tokenfile
+and configpaths by default. This is a convenience option to
+unset those values at init time. If those values are later
+changed, they may be used for future operations.
+
sslverify: Maps to 'requests' sslverify parameter. Set to
+False to disable SSL verification, but it can also be a path
+to file or directory for custom certs.
+
api_key: A bugzilla5+ API key
+
configpaths: A list of possible bugzillarc locations.
+
force_rest: Force use of the REST API
+
force_xmlrpc: Force use of the XMLRPC API. If neither force_X
+parameter are specified, heuristics will be used to determine
+which API to use, with XMLRPC preferred for back compatability.
+
requests_session: An optional requests.Session object the
+API will use to contact the remote bugzilla instance. This
+way the API user can set up whatever auth bits they may need.
+
+
+
+
+
+
+
+
+
@staticmethod
+
+ def
+ url_to_query(url):
+
+
+
+
+
+
104@staticmethod
+105defurl_to_query(url):
+106"""
+107 Given a big huge bugzilla query URL, returns a query dict that can
+108 be passed along to the Bugzilla.query() method.
+109 """
+110q={}
+111
+112# pylint: disable=unpacking-non-sequence
+113(ignore1,ignore2,path,
+114ignore,query,ignore3)=urllib.parse.urlparse(url)
+115
+116base=os.path.basename(path)
+117ifbasenotin('buglist.cgi','query.cgi'):
+118return{}
+119
+120for(k,v)inurllib.parse.parse_qsl(query):
+121ifknotinq:
+122q[k]=v
+123elifisinstance(q[k],list):
+124q[k].append(v)
+125else:
+126oldv=q[k]
+127q[k]=[oldv,v]
+128
+129# Handle saved searches
+130ifbase=="buglist.cgi"and"namedcmd"inqand"sharer_id"inq:
+131q={
+132"sharer_id":q["sharer_id"],
+133"savedsearch":q["namedcmd"],
+134}
+135
+136returnq
+
+
+
+
Given a big huge bugzilla query URL, returns a query dict that can
+be passed along to the Bugzilla.query() method.
+
+
+
+
+
+
+
+
@staticmethod
+
+ def
+ fix_url(url, force_rest=False):
+
+
+
+
+
+
138@staticmethod
+139deffix_url(url,force_rest=False):
+140"""
+141 Turn passed url into a bugzilla XMLRPC web url
+142
+143 :param force_rest: If True, generate a REST API url
+144 """
+145(scheme,netloc,path,
+146params,query,fragment)=urllib.parse.urlparse(url)
+147ifnotscheme:
+148scheme='https'
+149
+150ifpathandnotnetloc:
+151netloc=path.split("/",1)[0]
+152path="/".join(path.split("/")[1:])orNone
+153
+154ifnotpath:
+155path='xmlrpc.cgi'
+156ifforce_rest:
+157path="rest/"
+158
+159ifnotpath.startswith("/"):
+160path="/"+path
+161
+162newurl=urllib.parse.urlunparse(
+163(scheme,netloc,path,params,query,fragment))
+164returnnewurl
+
+
+
+
Turn passed url into a bugzilla XMLRPC web url
+
+
Parameters
+
+
+
force_rest: If True, generate a REST API url
+
+
+
+
+
+
+
+
+
@staticmethod
+
+ def
+ get_rcfile_default_url():
+
+
+
+
+
+
166@staticmethod
+167defget_rcfile_default_url():
+168"""
+169 Helper to check all the default bugzillarc file paths for
+170 a [DEFAULT] url=X section, and if found, return it.
+171 """
+172configpaths=_BugzillaRCFile.get_default_configpaths()
+173rcfile=_BugzillaRCFile()
+174rcfile.set_configpaths(configpaths)
+175returnrcfile.get_default_url()
+
+
+
+
Helper to check all the default bugzillarc file paths for
+a [DEFAULT] url=X section, and if found, return it.
392defreadconfig(self,configpath=None,overwrite=True):
+393"""
+394 :param configpath: Optional bugzillarc path to read, instead of
+395 the default list.
+396
+397 This function is called automatically from Bugzilla connect(), which
+398 is called at __init__ if a URL is passed. Calling it manually is
+399 just for passing in a non-standard configpath.
+400
+401 The locations for the bugzillarc file are preferred in this order:
+402
+403 ~/.config/python-bugzilla/bugzillarc
+404 ~/.bugzillarc
+405 /etc/bugzillarc
+406
+407 It has content like:
+408 [bugzilla.yoursite.com]
+409 user = username
+410 password = password
+411 Or
+412 [bugzilla.yoursite.com]
+413 api_key = key
+414
+415 The file can have multiple sections for different bugzilla instances.
+416 A 'url' field in the [DEFAULT] section can be used to set a default
+417 URL for the bugzilla command line tool.
+418
+419 Be sure to set appropriate permissions on bugzillarc if you choose to
+420 store your password in it!
+421
+422 :param overwrite: If True, bugzillarc will clobber any already
+423 set self.user/password/api_key/cert value.
+424 """
+425ifconfigpath:
+426self._setconfigpath(configpath)
+427data=self._rcfile.parse(self.url)
+428
+429forkey,valindata.items():
+430ifkey=="api_key"and(overwriteornotself.api_key):
+431log.debug("bugzillarc: setting api_key")
+432self.api_key=val
+433elifkey=="user"and(overwriteornotself.user):
+434log.debug("bugzillarc: setting user=%s",val)
+435self.user=val
+436elifkey=="password"and(overwriteornotself.password):
+437log.debug("bugzillarc: setting password")
+438self.password=val
+439elifkey=="cert"and(overwriteornotself.cert):
+440log.debug("bugzillarc: setting cert")
+441self.cert=val
+442else:
+443log.debug("bugzillarc: unknown key=%s",key)
+
+
+
+
Parameters
+
+
+
configpath: Optional bugzillarc path to read, instead of
+the default list.
+
+
+
This function is called automatically from Bugzilla connect(), which
+is called at __init__ if a URL is passed. Calling it manually is
+just for passing in a non-standard configpath.
+
+
The locations for the bugzillarc file are preferred in this order:
It has content like:
+ [bugzilla.yoursite.com]
+ user = username
+ password = password
+Or
+ [bugzilla.yoursite.com]
+ api_key = key
+
+
The file can have multiple sections for different bugzilla instances.
+A 'url' field in the [DEFAULT] section can be used to set a default
+URL for the bugzilla command line tool.
+
+
Be sure to set appropriate permissions on bugzillarc if you choose to
+store your password in it!
+
+
+
overwrite: If True, bugzillarc will clobber any already
+set self.user/password/api_key/cert value.
+
+
+
+
+
+
+
+
+
+ def
+ connect(self, url=None):
+
+
+
+
+
+
486defconnect(self,url=None):
+487"""
+488 Connect to the bugzilla instance with the given url. This is
+489 called by __init__ if a URL is passed. Or it can be called manually
+490 at any time with a passed URL.
+491
+492 This will also read any available config files (see readconfig()),
+493 which may set 'user' and 'password', and others.
+494
+495 If 'user' and 'password' are both set, we'll run login(). Otherwise
+496 you'll have to login() yourself before some methods will work.
+497 """
+498ifself._session:
+499self.disconnect()
+500
+501url=urlorself.url
+502backendclass,newurl=self._get_backend_class(url)
+503ifurl!=newurl:
+504log.debug("Converted url=%s to fixed url=%s",url,newurl)
+505self.url=newurl
+506log.debug("Connecting with URL %s",self.url)
+507
+508# we've changed URLs - reload config
+509self.readconfig(overwrite=False)
+510
+511# Detect if connecting to redhat bugzilla
+512self._init_class_from_url()
+513
+514self._session=_BugzillaSession(self.url,self.user_agent,
+515sslverify=self._sslverify,
+516cert=self.cert,
+517tokencache=self._tokencache,
+518api_key=self.api_key,
+519is_redhat_bugzilla=self._is_redhat_bugzilla,
+520requests_session=self._user_requests_session)
+521self._backend=backendclass(self.url,self._session)
+522
+523if(self.userandself.password):
+524log.info("user and password present - doing login()")
+525self.login()
+526
+527ifself.api_key:
+528log.debug("using API key")
+529
+530version=self._backend.bugzilla_version()["version"]
+531log.debug("Bugzilla version string: %s",version)
+532self._set_bz_version(version)
+
+
+
+
Connect to the bugzilla instance with the given url. This is
+called by __init__ if a URL is passed. Or it can be called manually
+at any time with a passed URL.
+
+
This will also read any available config files (see readconfig()),
+which may set 'user' and 'password', and others.
+
+
If 'user' and 'password' are both set, we'll run login(). Otherwise
+you'll have to login() yourself before some methods will work.
+
+
+
+
+
+
+
+
+ def
+ is_xmlrpc(self):
+
+
+
+
+
+
546defis_xmlrpc(self):
+547"""
+548 :returns: True if using the XMLRPC API
+549 """
+550returnself._backend.is_xmlrpc()
+
+
+
+
:returns: True if using the XMLRPC API
+
+
+
+
+
+
+
+
+ def
+ is_rest(self):
+
+
+
+
+
+
552defis_rest(self):
+553"""
+554 :returns: True if using the REST API
+555 """
+556returnself._backend.is_rest()
+
+
+
+
:returns: True if using the REST API
+
+
+
+
+
+
+
+
+ def
+ get_requests_session(self):
+
+
+
+
+
+
558defget_requests_session(self):
+559"""
+560 Give API users access to the Requests.session object we use for
+561 talking to the remote bugzilla instance.
+562
+563 :returns: The Requests.session object backing the open connection.
+564 """
+565returnself._session.get_requests_session()
+
+
+
+
Give API users access to the Requests.session object we use for
+talking to the remote bugzilla instance.
+
+
:returns: The Requests.session object backing the open connection.
+
+
+
+
+
+
+
+
+ def
+ disconnect(self):
+
+
+
+
+
+
567defdisconnect(self):
+568"""
+569 Disconnect from the given bugzilla instance.
+570 """
+571self._backend=None
+572self._session=None
+573self._cache=_BugzillaAPICache()
+
575deflogin(self,user=None,password=None,restrict_login=None):
+576"""
+577 Attempt to log in using the given username and password. Subsequent
+578 method calls will use this username and password. Returns False if
+579 login fails, otherwise returns some kind of login info - typically
+580 either a numeric userid, or a dict of user info.
+581
+582 If user is not set, the value of Bugzilla.user will be used. If *that*
+583 is not set, ValueError will be raised. If login fails, BugzillaError
+584 will be raised.
+585
+586 The login session can be restricted to current user IP address
+587 with restrict_login argument. (Bugzilla 4.4+)
+588
+589 This method will be called implicitly at the end of connect() if user
+590 and password are both set. So under most circumstances you won't need
+591 to call this yourself.
+592 """
+593ifself.api_key:
+594raiseValueError("cannot login when using an API key")
+595
+596ifuser:
+597self.user=user
+598ifpassword:
+599self.password=password
+600
+601ifnotself.user:
+602raiseValueError("missing username")
+603ifnotself.password:
+604raiseValueError("missing password")
+605
+606payload={"login":self.user}
+607ifrestrict_login:
+608payload['restrict_login']=True
+609log.debug("logging in with options %s",str(payload))
+610payload['password']=self.password
+611
+612try:
+613ret=self._backend.user_login(payload)
+614self.password=''
+615log.info("login succeeded for user=%s",self.user)
+616if"token"inret:
+617self._tokencache.set_value(self.url,ret["token"])
+618returnret
+619exceptExceptionase:
+620log.debug("Login exception: %s",str(e),exc_info=True)
+621raiseBugzillaError("Login failed: %s"%
+622BugzillaError.get_bugzilla_error_string(e))fromNone
+
+
+
+
Attempt to log in using the given username and password. Subsequent
+method calls will use this username and password. Returns False if
+login fails, otherwise returns some kind of login info - typically
+either a numeric userid, or a dict of user info.
+
+
If user is not set, the value of Bugzilla.user will be used. If that
+is not set, ValueError will be raised. If login fails, BugzillaError
+will be raised.
+
+
The login session can be restricted to current user IP address
+with restrict_login argument. (Bugzilla 4.4+)
+
+
This method will be called implicitly at the end of connect() if user
+and password are both set. So under most circumstances you won't need
+to call this yourself.
+
+
+
+
+
+
+
+
+ def
+ interactive_save_api_key(self):
+
+
+
+
+
+
624definteractive_save_api_key(self):
+625"""
+626 Helper method to interactively ask for an API key, verify it
+627 is valid, and save it to a bugzillarc file referenced via
+628 self.configpaths
+629 """
+630sys.stdout.write('API Key: ')
+631sys.stdout.flush()
+632api_key=sys.stdin.readline().strip()
+633
+634self.disconnect()
+635self.api_key=api_key
+636
+637log.info('Checking API key... ')
+638self.connect()
+639
+640ifnotself.logged_in:# pragma: no cover
+641raiseBugzillaError("Login with API_KEY failed")
+642log.info('API Key accepted')
+643
+644wrote_filename=self._rcfile.save_api_key(self.url,self.api_key)
+645log.info("API key written to filename=%s",wrote_filename)
+646
+647msg="Login successful."
+648ifwrote_filename:
+649msg+=" API key written to %s"%wrote_filename
+650print(msg)
+
+
+
+
Helper method to interactively ask for an API key, verify it
+is valid, and save it to a bugzillarc file referenced via
+self.configpaths
652definteractive_login(self,user=None,password=None,force=False,
+653restrict_login=None):
+654"""
+655 Helper method to handle login for this bugzilla instance.
+656
+657 :param user: bugzilla username. If not specified, prompt for it.
+658 :param password: bugzilla password. If not specified, prompt for it.
+659 :param force: Unused
+660 :param restrict_login: restricts session to IP address
+661 """
+662ignore=force
+663log.debug('Calling interactive_login')
+664
+665ifnotuser:
+666sys.stdout.write('Bugzilla Username: ')
+667sys.stdout.flush()
+668user=sys.stdin.readline().strip()
+669ifnotpassword:
+670password=getpass.getpass('Bugzilla Password: ')
+671
+672log.info('Logging in... ')
+673out=self.login(user,password,restrict_login)
+674msg="Login successful."
+675if"token"notinout:
+676msg+=" However no token was returned."
+677else:
+678ifnotself.tokenfile:
+679msg+=" Token not saved to disk."
+680else:
+681msg+=" Token cache saved to %s"%self.tokenfile
+682ifself._get_version()>=5.0:
+683msg+="\nToken usage is deprecated. "
+684msg+="Consider using bugzilla API keys instead. "
+685msg+="See `man bugzilla` for more details."
+686print(msg)
+
+
+
+
Helper method to handle login for this bugzilla instance.
+
+
Parameters
+
+
+
user: bugzilla username. If not specified, prompt for it.
+
password: bugzilla password. If not specified, prompt for it.
+
force: Unused
+
restrict_login: restricts session to IP address
+
+
+
+
+
+
+
+
+
+ def
+ logout(self):
+
+
+
+
+
+
688deflogout(self):
+689"""
+690 Log out of bugzilla. Drops server connection and user info, and
+691 destroys authentication cache
+692 """
+693self._backend.user_logout()
+694self.disconnect()
+695self.user=''
+696self.password=''
+
+
+
+
Log out of bugzilla. Drops server connection and user info, and
+destroys authentication cache
+
+
+
+
+
+
+
+ logged_in
+
+
+
+
+
+
698@property
+699deflogged_in(self):
+700"""
+701 This is True if this instance is logged in else False.
+702
+703 We test if this session is authenticated by calling the User.get()
+704 XMLRPC method with ids set. Logged-out users cannot pass the 'ids'
+705 parameter and will result in a 505 error. If we tried to login with a
+706 token, but the token was incorrect or expired, the server returns a
+707 32000 error.
+708
+709 For Bugzilla 5 and later, a new method, User.valid_login is available
+710 to test the validity of the token. However, this will require that the
+711 username be cached along with the token in order to work effectively in
+712 all scenarios and is not currently used. For more information, refer to
+713 the following url.
+714
+715 http://bugzilla.readthedocs.org/en/latest/api/core/v1/user.html#valid-login
+716 """
+717try:
+718self._backend.user_get({"ids":[1]})
+719returnTrue
+720exceptExceptionase:
+721code=BugzillaError.get_bugzilla_error_code(e)
+722ifcodein[505,32000]:
+723returnFalse
+724raisee
+
+
+
+
This is True if this instance is logged in else False.
+
+
We test if this session is authenticated by calling the User.get()
+XMLRPC method with ids set. Logged-out users cannot pass the 'ids'
+parameter and will result in a 505 error. If we tried to login with a
+token, but the token was incorrect or expired, the server returns a
+32000 error.
+
+
For Bugzilla 5 and later, a new method, User.valid_login is available
+to test the validity of the token. However, this will require that the
+username be cached along with the token in order to work effectively in
+all scenarios and is not currently used. For more information, refer to
+the following url.
731defgetbugfields(self,force_refresh=False,names=None):
+732"""
+733 Calls getBugFields, which returns a list of fields in each bug
+734 for this bugzilla instance. This can be used to set the list of attrs
+735 on the Bug object.
+736
+737 :param force_refresh: If True, overwrite the bugfield cache
+738 with these newly checked values.
+739 :param names: Only check for the passed bug field names
+740 """
+741def_fieldnames():
+742data={"include_fields":["name"]}
+743ifnames:
+744data["names"]=names
+745r=self._backend.bug_fields(data)
+746return[f['name']forfinr['fields']]
+747
+748ifforce_refreshornotself._cache.bugfields:
+749log.debug("Refreshing bugfields")
+750self._cache.bugfields=_fieldnames()
+751self._cache.bugfields.sort()
+752log.debug("bugfields = %s",self._cache.bugfields)
+753
+754returnself._cache.bugfields
+
+
+
+
Calls getBugFields, which returns a list of fields in each bug
+for this bugzilla instance. This can be used to set the list of attrs
+on the Bug object.
+
+
Parameters
+
+
+
force_refresh: If True, overwrite the bugfield cache
+with these newly checked values.
763defproduct_get(self,ids=None,names=None,
+764include_fields=None,exclude_fields=None,
+765ptype=None):
+766"""
+767 Raw wrapper around Product.get
+768 https://bugzilla.readthedocs.io/en/latest/api/core/v1/product.html#get-product
+769
+770 This does not perform any caching like other product API calls.
+771 If ids, names, or ptype is not specified, we default to
+772 ptype=accessible for historical reasons
+773
+774 @ids: List of product IDs to lookup
+775 @names: List of product names to lookup
+776 @ptype: Either 'accessible', 'selectable', or 'enterable'. If
+777 specified, we return data for all those
+778 @include_fields: Only include these fields in the output
+779 @exclude_fields: Do not include these fields in the output
+780 """
+781ifidsisNoneandnamesisNoneandptypeisNone:
+782ptype="accessible"
+783
+784ifptype:
+785raw=None
+786ifptype=="accessible":
+787raw=self._backend.product_get_accessible()
+788elifptype=="enterable":
+789raw=self._backend.product_get_enterable()
+790elifptype=="selectable":
+791raw=self._backend.product_get_selectable()
+792
+793ifrawisNone:
+794raiseRuntimeError("Unknown ptype=%s"%ptype)
+795ids=raw['ids']
+796log.debug("For ptype=%s found ids=%s",ptype,ids)
+797
+798kwargs={}
+799ifids:
+800kwargs["ids"]=listify(ids)
+801ifnames:
+802kwargs["names"]=listify(names)
+803ifinclude_fields:
+804kwargs["include_fields"]=include_fields
+805ifexclude_fields:
+806kwargs["exclude_fields"]=exclude_fields
+807
+808ret=self._backend.product_get(kwargs)
+809returnret['products']
+
This does not perform any caching like other product API calls.
+If ids, names, or ptype is not specified, we default to
+ptype=accessible for historical reasons
+
+
@ids: List of product IDs to lookup
+@names: List of product names to lookup
+@ptype: Either 'accessible', 'selectable', or 'enterable'. If
+ specified, we return data for all those
+@include_fields: Only include these fields in the output
+@exclude_fields: Do not include these fields in the output
811defrefresh_products(self,**kwargs):
+812"""
+813 Refresh a product's cached info. Basically calls product_get
+814 with the passed arguments, and tries to intelligently update
+815 our product cache.
+816
+817 For example, if we already have cached info for product=foo,
+818 and you pass in names=["bar", "baz"], the new cache will have
+819 info for products foo, bar, baz. Individual product fields are
+820 also updated.
+821 """
+822forproductinself.product_get(**kwargs):
+823updated=False
+824forcurrentinself._cache.products[:]:
+825if(current.get("id",-1)!=product.get("id",-2)and
+826current.get("name",-1)!=product.get("name",-2)):
+827continue
+828
+829_nested_update(current,product)
+830updated=True
+831break
+832ifnotupdated:
+833self._cache.products.append(product)
+
+
+
+
Refresh a product's cached info. Basically calls product_get
+with the passed arguments, and tries to intelligently update
+our product cache.
+
+
For example, if we already have cached info for product=foo,
+and you pass in names=["bar", "baz"], the new cache will have
+info for products foo, bar, baz. Individual product fields are
+also updated.
835defgetproducts(self,force_refresh=False,**kwargs):
+836"""
+837 Query all products and return the raw dict info. Takes all the
+838 same arguments as product_get.
+839
+840 On first invocation this will contact bugzilla and internally
+841 cache the results. Subsequent getproducts calls or accesses to
+842 self.products will return this cached data only.
+843
+844 :param force_refresh: force refreshing via refresh_products()
+845 """
+846ifforce_refreshornotself._cache.products:
+847self.refresh_products(**kwargs)
+848returnself._cache.products
+
+
+
+
Query all products and return the raw dict info. Takes all the
+same arguments as product_get.
+
+
On first invocation this will contact bugzilla and internally
+cache the results. Subsequent getproducts calls or accesses to
+self.products will return this cached data only.
+
+
Parameters
+
+
+
force_refresh: force refreshing via refresh_products()
871defgetcomponentsdetails(self,product,force_refresh=False):
+872"""
+873 Wrapper around Product.get(include_fields=["components"]),
+874 returning only the "components" data for the requested product,
+875 slightly reworked to a dict mapping of components.name: components,
+876 for historical reasons.
+877
+878 This uses the product cache, but will update it if the product
+879 isn't found or "components" isn't cached for the product.
+880
+881 In cases like bugzilla.redhat.com where there are tons of
+882 components for some products, this API will time out. You
+883 should use product_get instead.
+884 """
+885proddict=self._lookup_product_in_cache(product)
+886
+887if(force_refreshornotproddictor"components"notinproddict):
+888self.refresh_products(names=[product],
+889include_fields=["name","id","components"])
+890proddict=self._lookup_product_in_cache(product)
+891
+892ret={}
+893forcompdictinproddict["components"]:
+894ret[compdict["name"]]=compdict
+895returnret
+
+
+
+
Wrapper around Product.get(include_fields=["components"]),
+returning only the "components" data for the requested product,
+slightly reworked to a dict mapping of components.name: components,
+for historical reasons.
+
+
This uses the product cache, but will update it if the product
+isn't found or "components" isn't cached for the product.
+
+
In cases like bugzilla.redhat.com where there are tons of
+components for some products, this API will time out. You
+should use product_get instead.
897defgetcomponentdetails(self,product,component,force_refresh=False):
+898"""
+899 Helper for accessing a single component's info. This is a wrapper
+900 around getcomponentsdetails, see that for explanation
+901 """
+902d=self.getcomponentsdetails(product,force_refresh)
+903returnd[component]
+
+
+
+
Helper for accessing a single component's info. This is a wrapper
+around getcomponentsdetails, see that for explanation
905defgetcomponents(self,product,force_refresh=False):
+906"""
+907 Return a list of component names for the passed product.
+908
+909 On first invocation the value is cached, and subsequent calls
+910 will return the cached data.
+911
+912 :param force_refresh: Force refreshing the cache, and return
+913 the new data
+914 """
+915proddict=self._lookup_product_in_cache(product)
+916product_id=proddict.get("id",None)
+917
+918if(force_refreshorproduct_idisNoneor
+919"components"notinproddict):
+920self.refresh_products(
+921names=[product],
+922include_fields=["name","id","components.name"])
+923proddict=self._lookup_product_in_cache(product)
+924if"id"notinproddict:
+925raiseBugzillaError("Product '%s' not found"%product)
+926product_id=proddict["id"]
+927
+928ifproduct_idnotinself._cache.component_names:
+929names=[]
+930forcompinproddict.get("components",[]):
+931name=comp.get("name")
+932ifname:
+933names.append(name)
+934self._cache.component_names[product_id]=names
+935
+936returnself._cache.component_names[product_id]
+
+
+
+
Return a list of component names for the passed product.
+
+
On first invocation the value is cached, and subsequent calls
+will return the cached data.
+
+
Parameters
+
+
+
force_refresh: Force refreshing the cache, and return
+the new data
+
+
+
+
+
+
+
+
+
+ def
+ addcomponent(self, data):
+
+
+
+
+
+
965defaddcomponent(self,data):
+966"""
+967 A method to create a component in Bugzilla. Takes a dict, with the
+968 following elements:
+969
+970 product: The product to create the component in
+971 component: The name of the component to create
+972 description: A one sentence summary of the component
+973 default_assignee: The bugzilla login (email address) of the initial
+974 owner of the component
+975 default_qa_contact (optional): The bugzilla login of the
+976 initial QA contact
+977 default_cc: (optional) The initial list of users to be CC'ed on
+978 new bugs for the component.
+979 is_active: (optional) If False, the component is hidden from
+980 the component list when filing new bugs.
+981 """
+982data=data.copy()
+983self._component_data_convert(data)
+984returnself._backend.component_create(data)
+
+
+
+
A method to create a component in Bugzilla. Takes a dict, with the
+following elements:
+
+
product: The product to create the component in
+component: The name of the component to create
+description: A one sentence summary of the component
+default_assignee: The bugzilla login (email address) of the initial
+ owner of the component
+default_qa_contact (optional): The bugzilla login of the
+ initial QA contact
+default_cc: (optional) The initial list of users to be CC'ed on
+ new bugs for the component.
+is_active: (optional) If False, the component is hidden from
+ the component list when filing new bugs.
+
+
+
+
+
+
+
+
+ def
+ editcomponent(self, data):
+
+
+
+
+
+
986defeditcomponent(self,data):
+987"""
+988 A method to edit a component in Bugzilla. Takes a dict, with
+989 mandatory elements of product. component, and initialowner.
+990 All other elements are optional and use the same names as the
+991 addcomponent() method.
+992 """
+993data=data.copy()
+994self._component_data_convert(data,update=True)
+995returnself._backend.component_update(data)
+
+
+
+
A method to edit a component in Bugzilla. Takes a dict, with
+mandatory elements of product. component, and initialowner.
+All other elements are optional and use the same names as the
+addcomponent() method.
+
+
+
+
+
+
+
+ bug_autorefresh
+
+
+
+
+
+
1029def_get_bug_autorefresh(self):
+1030"""
+1031 This value is passed to Bug.autorefresh for all fetched bugs.
+1032 If True, and an uncached attribute is requested from a Bug,
+1033 the Bug will update its contents and try again.
+1034 """
+1035returnself._bug_autorefresh
+
+
+
+
This value is passed to Bug.autorefresh for all fetched bugs.
+If True, and an uncached attribute is requested from a Bug,
+ the Bug will update its contents and try again.
1132defgetbug(self,objid,
+1133include_fields=None,exclude_fields=None,extra_fields=None):
+1134"""
+1135 Return a Bug object with the full complement of bug data
+1136 already loaded.
+1137 """
+1138data=self._getbug(objid,
+1139include_fields=include_fields,exclude_fields=exclude_fields,
+1140extra_fields=extra_fields)
+1141returnBug(self,dict=data,autorefresh=self.bug_autorefresh)
+
+
+
+
Return a Bug object with the full complement of bug data
+already loaded.
1143defgetbugs(self,idlist,
+1144include_fields=None,exclude_fields=None,extra_fields=None,
+1145permissive=True):
+1146"""
+1147 Return a list of Bug objects with the full complement of bug data
+1148 already loaded. If there's a problem getting the data for a given id,
+1149 the corresponding item in the returned list will be None.
+1150 """
+1151data=self._getbugs(idlist,include_fields=include_fields,
+1152exclude_fields=exclude_fields,extra_fields=extra_fields,
+1153permissive=permissive)
+1154return[(bandBug(self,dict=b,
+1155autorefresh=self.bug_autorefresh))orNone
+1156forbindata]
+
+
+
+
Return a list of Bug objects with the full complement of bug data
+already loaded. If there's a problem getting the data for a given id,
+the corresponding item in the returned list will be None.
+
+
+
+
+
+
+
+
+ def
+ get_comments(self, idlist):
+
+
+
+
+
+
1158defget_comments(self,idlist):
+1159"""
+1160 Returns a dictionary of bugs and comments. The comments key will
+1161 be empty. See bugzilla docs for details
+1162 """
+1163returnself._backend.bug_comments(idlist,{})
+
+
+
+
Returns a dictionary of bugs and comments. The comments key will
+be empty. See bugzilla docs for details
1170defbuild_query(self,
+1171product=None,
+1172component=None,
+1173version=None,
+1174long_desc=None,
+1175bug_id=None,
+1176short_desc=None,
+1177cc=None,
+1178assigned_to=None,
+1179reporter=None,
+1180qa_contact=None,
+1181status=None,
+1182blocked=None,
+1183dependson=None,
+1184keywords=None,
+1185keywords_type=None,
+1186url=None,
+1187url_type=None,
+1188status_whiteboard=None,
+1189status_whiteboard_type=None,
+1190fixed_in=None,
+1191fixed_in_type=None,
+1192flag=None,
+1193alias=None,
+1194qa_whiteboard=None,
+1195devel_whiteboard=None,
+1196bug_severity=None,
+1197priority=None,
+1198target_release=None,
+1199target_milestone=None,
+1200emailtype=None,
+1201include_fields=None,
+1202quicksearch=None,
+1203savedsearch=None,
+1204savedsearch_sharer_id=None,
+1205sub_component=None,
+1206tags=None,
+1207exclude_fields=None,
+1208extra_fields=None,
+1209limit=None,
+1210resolution=None):
+1211"""
+1212 Build a query string from passed arguments. Will handle
+1213 query parameter differences between various bugzilla versions.
+1214
+1215 Most of the parameters should be self-explanatory. However,
+1216 if you want to perform a complex query, and easy way is to
+1217 create it with the bugzilla web UI, copy the entire URL it
+1218 generates, and pass it to the static method
+1219
+1220 Bugzilla.url_to_query
+1221
+1222 Then pass the output to Bugzilla.query()
+1223
+1224 For details about the specific argument formats, see the bugzilla docs:
+1225 https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#search-bugs
+1226 """
+1227query={
+1228"alias":alias,
+1229"product":listify(product),
+1230"component":listify(component),
+1231"version":version,
+1232"id":bug_id,
+1233"short_desc":short_desc,
+1234"bug_status":status,
+1235"bug_severity":bug_severity,
+1236"priority":priority,
+1237"target_release":target_release,
+1238"target_milestone":target_milestone,
+1239"tag":listify(tags),
+1240"quicksearch":quicksearch,
+1241"savedsearch":savedsearch,
+1242"sharer_id":savedsearch_sharer_id,
+1243"limit":limit,
+1244"resolution":resolution,
+1245
+1246# RH extensions... don't add any more. See comment below
+1247"sub_components":listify(sub_component),
+1248}
+1249
+1250defadd_bool(bzkey,value,bool_id,booltype=None):
+1251value=listify(value)
+1252ifvalueisNone:
+1253returnbool_id
+1254
+1255query["query_format"]="advanced"
+1256forboolvalinvalue:
+1257defmake_bool_str(prefix):
+1258# pylint: disable=cell-var-from-loop
+1259return"%s%i-0-0"%(prefix,bool_id)
+1260
+1261query[make_bool_str("field")]=bzkey
+1262query[make_bool_str("value")]=boolval
+1263query[make_bool_str("type")]=booltypeor"substring"
+1264
+1265bool_id+=1
+1266returnbool_id
+1267
+1268# RH extensions that we have to maintain here for back compat,
+1269# but all future custom fields should be specified via
+1270# cli --field option, or via extending the query dict() manually.
+1271# No more supporting custom fields in this API
+1272bool_id=0
+1273bool_id=add_bool("keywords",keywords,bool_id,keywords_type)
+1274bool_id=add_bool("blocked",blocked,bool_id)
+1275bool_id=add_bool("dependson",dependson,bool_id)
+1276bool_id=add_bool("bug_file_loc",url,bool_id,url_type)
+1277bool_id=add_bool("cf_fixed_in",fixed_in,bool_id,fixed_in_type)
+1278bool_id=add_bool("flagtypes.name",flag,bool_id)
+1279bool_id=add_bool("status_whiteboard",
+1280status_whiteboard,bool_id,status_whiteboard_type)
+1281bool_id=add_bool("cf_qa_whiteboard",qa_whiteboard,bool_id)
+1282bool_id=add_bool("cf_devel_whiteboard",devel_whiteboard,bool_id)
+1283
+1284defadd_email(key,value,count):
+1285ifvalueisNone:
+1286returncount
+1287ifnotemailtype:
+1288query[key]=value
+1289returncount
+1290
+1291query["query_format"]="advanced"
+1292query['email%i'%count]=value
+1293query['email%s%i'%(key,count)]=True
+1294query['emailtype%i'%count]=emailtype
+1295returncount+1
+1296
+1297email_count=1
+1298email_count=add_email("cc",cc,email_count)
+1299email_count=add_email("assigned_to",assigned_to,email_count)
+1300email_count=add_email("reporter",reporter,email_count)
+1301email_count=add_email("qa_contact",qa_contact,email_count)
+1302
+1303iflong_descisnotNone:
+1304query["query_format"]="advanced"
+1305query["longdesc"]=long_desc
+1306query["longdesc_type"]="allwordssubstr"
+1307
+1308# 'include_fields' only available for Bugzilla4+
+1309# 'extra_fields' is an RHBZ extension
+1310query.update(self._process_include_fields(
+1311include_fields,exclude_fields,extra_fields))
+1312
+1313# Strip out None elements in the dict
+1314fork,vinquery.copy().items():
+1315ifvisNone:
+1316delquery[k]
+1317
+1318self.pre_translation(query)
+1319returnquery
+
+
+
+
Build a query string from passed arguments. Will handle
+query parameter differences between various bugzilla versions.
+
+
Most of the parameters should be self-explanatory. However,
+if you want to perform a complex query, and easy way is to
+create it with the bugzilla web UI, copy the entire URL it
+generates, and pass it to the static method
1322defquery_return_extra(self,query):
+1323"""
+1324 Same as `query()`, but the return value is altered to be
+1325 (buglist, values), where `values` is raw dictionary output from
+1326 the API call, excluding the bug content. For example this may
+1327 include a `limit` value if the bugzilla instance puts an implied
+1328 limit on returned result numbers.
+1329 """
+1330try:
+1331r=self._backend.bug_search(query)
+1332log.debug("bug_search returned:\n%s",str(r))
+1333exceptExceptionase:
+1334# Try to give a hint in the error message if url_to_query
+1335# isn't supported by this bugzilla instance
+1336if("query_format"notinstr(e)or
+1337notBugzillaError.get_bugzilla_error_code(e)or
+1338self._get_version()>=5.0):
+1339raise
+1340raiseBugzillaError("%s\nYour bugzilla instance does not "
+1341"appear to support API queries derived from bugzilla "
+1342"web URL queries."%e)fromNone
+1343
+1344rawbugs=r.pop("bugs")
+1345log.debug("Query returned %s bugs",len(rawbugs))
+1346bugs=[Bug(self,dict=b,
+1347autorefresh=self.bug_autorefresh)forbinrawbugs]
+1348
+1349returnbugs,r
+
+
+
+
Same as query(), but the return value is altered to be
+(buglist, values), where values is raw dictionary output from
+the API call, excluding the bug content. For example this may
+include a limit value if the bugzilla instance puts an implied
+limit on returned result numbers.
+
+
+
+
+
+
+
+
+ def
+ query(self, query):
+
+
+
+
+
+
1351defquery(self,query):
+1352"""
+1353 Pass search terms to bugzilla and and return a list of matching
+1354 Bug objects.
+1355
+1356 See `build_query` for more details about constructing the
+1357 `query` dict parameter.
+1358 """
+1359bugs,dummy=self.query_return_extra(query)
+1360returnbugs
+
+
+
+
Pass search terms to bugzilla and and return a list of matching
+Bug objects.
+
+
See build_query for more details about constructing the
+query dict parameter.
+
+
+
+
+
+
+
+
+ def
+ pre_translation(self, query):
+
+
+
+
+
+
1362defpre_translation(self,query):
+1363"""
+1364 In order to keep the API the same, Bugzilla4 needs to process the
+1365 query and the result. This also applies to the refresh() function
+1366 """
+1367ifself._is_redhat_bugzilla:
+1368_RHBugzillaConverters.pre_translation(query)
+1369query.update(self._process_include_fields(
+1370query.get("include_fields",[]),None,None))
+
+
+
+
In order to keep the API the same, Bugzilla4 needs to process the
+query and the result. This also applies to the refresh() function
1372defpost_translation(self,query,bug):
+1373"""
+1374 In order to keep the API the same, Bugzilla4 needs to process the
+1375 query and the result. This also applies to the refresh() function
+1376 """
+1377ifself._is_redhat_bugzilla:
+1378_RHBugzillaConverters.post_translation(query,bug)
+
+
+
+
In order to keep the API the same, Bugzilla4 needs to process the
+query and the result. This also applies to the refresh() function
1380defbugs_history_raw(self,bug_ids):
+1381"""
+1382 Experimental. Gets the history of changes for
+1383 particular bugs in the database.
+1384 """
+1385returnself._backend.bug_history(bug_ids,{})
+
+
+
+
Experimental. Gets the history of changes for
+particular bugs in the database.
1394defupdate_bugs(self,ids,updates):
+1395"""
+1396 A thin wrapper around bugzilla Bug.update(). Used to update all
+1397 values of an existing bug report, as well as add comments.
+1398
+1399 The dictionary passed to this function should be generated with
+1400 build_update(), otherwise we cannot guarantee back compatibility.
+1401 """
+1402tmp=updates.copy()
+1403returnself._backend.bug_update(listify(ids),tmp)
+
+
+
+
A thin wrapper around bugzilla Bug.update(). Used to update all
+values of an existing bug report, as well as add comments.
+
+
The dictionary passed to this function should be generated with
+build_update(), otherwise we cannot guarantee back compatibility.
1421defupdate_flags(self,idlist,flags):
+1422"""
+1423 A thin back compat wrapper around build_update(flags=X)
+1424 """
+1425returnself.update_bugs(idlist,self.build_update(flags=flags))
+
+
+
+
A thin back compat wrapper around build_update(flags=X)
1428defbuild_update(self,
+1429alias=None,
+1430assigned_to=None,
+1431blocks_add=None,
+1432blocks_remove=None,
+1433blocks_set=None,
+1434depends_on_add=None,
+1435depends_on_remove=None,
+1436depends_on_set=None,
+1437cc_add=None,
+1438cc_remove=None,
+1439is_cc_accessible=None,
+1440comment=None,
+1441comment_private=None,
+1442component=None,
+1443deadline=None,
+1444dupe_of=None,
+1445estimated_time=None,
+1446groups_add=None,
+1447groups_remove=None,
+1448keywords_add=None,
+1449keywords_remove=None,
+1450keywords_set=None,
+1451op_sys=None,
+1452platform=None,
+1453priority=None,
+1454product=None,
+1455qa_contact=None,
+1456is_creator_accessible=None,
+1457remaining_time=None,
+1458reset_assigned_to=None,
+1459reset_qa_contact=None,
+1460resolution=None,
+1461see_also_add=None,
+1462see_also_remove=None,
+1463severity=None,
+1464status=None,
+1465summary=None,
+1466target_milestone=None,
+1467target_release=None,
+1468url=None,
+1469version=None,
+1470whiteboard=None,
+1471work_time=None,
+1472fixed_in=None,
+1473qa_whiteboard=None,
+1474devel_whiteboard=None,
+1475internal_whiteboard=None,
+1476sub_component=None,
+1477flags=None,
+1478comment_tags=None,
+1479minor_update=None):
+1480"""
+1481 Returns a python dict() with properly formatted parameters to
+1482 pass to update_bugs(). See bugzilla documentation for the format
+1483 of the individual fields:
+1484
+1485 https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#create-bug
+1486 """
+1487ret={}
+1488rhbzret={}
+1489
+1490# These are only supported for rhbugzilla
+1491#
+1492# This should not be extended any more.
+1493# If people want to handle custom fields, manually extend the
+1494# returned dictionary.
+1495rhbzargs={
+1496"fixed_in":fixed_in,
+1497"devel_whiteboard":devel_whiteboard,
+1498"qa_whiteboard":qa_whiteboard,
+1499"internal_whiteboard":internal_whiteboard,
+1500"sub_component":sub_component,
+1501}
+1502ifself._is_redhat_bugzilla:
+1503rhbzret=_RHBugzillaConverters.convert_build_update(
+1504component=component,**rhbzargs)
+1505else:
+1506forkey,valinrhbzargs.items():
+1507ifvalisnotNone:
+1508raiseValueError("bugzilla instance does not support "
+1509"updating '%s'"%key)
+1510
+1511defs(key,val,convert=None):
+1512ifvalisNone:
+1513return
+1514ifconvert:
+1515val=convert(val)
+1516ret[key]=val
+1517
+1518defadd_dict(key,add,remove,_set=None):
+1519ifaddisremoveis_setisNone:
+1520return
+1521
+1522newdict={}
+1523ifaddisnotNone:
+1524newdict["add"]=listify(add)
+1525ifremoveisnotNone:
+1526newdict["remove"]=listify(remove)
+1527if_setisnotNone:
+1528newdict["set"]=listify(_set)
+1529ret[key]=newdict
+1530
+1531
+1532s("alias",alias)
+1533s("assigned_to",assigned_to)
+1534s("is_cc_accessible",is_cc_accessible,bool)
+1535s("component",component)
+1536s("deadline",deadline)
+1537s("dupe_of",dupe_of,int)
+1538s("estimated_time",estimated_time,int)
+1539s("op_sys",op_sys)
+1540s("platform",platform)
+1541s("priority",priority)
+1542s("product",product)
+1543s("qa_contact",qa_contact)
+1544s("is_creator_accessible",is_creator_accessible,bool)
+1545s("remaining_time",remaining_time,float)
+1546s("reset_assigned_to",reset_assigned_to,bool)
+1547s("reset_qa_contact",reset_qa_contact,bool)
+1548s("resolution",resolution)
+1549s("severity",severity)
+1550s("status",status)
+1551s("summary",summary)
+1552s("target_milestone",target_milestone)
+1553s("target_release",target_release)
+1554s("url",url)
+1555s("version",version)
+1556s("whiteboard",whiteboard)
+1557s("work_time",work_time,float)
+1558s("flags",flags)
+1559s("comment_tags",comment_tags,listify)
+1560s("minor_update",minor_update,bool)
+1561
+1562add_dict("blocks",blocks_add,blocks_remove,blocks_set)
+1563add_dict("depends_on",depends_on_add,depends_on_remove,depends_on_set)
+1564add_dict("cc",cc_add,cc_remove)
+1565add_dict("groups",groups_add,groups_remove)
+1566add_dict("keywords",keywords_add,keywords_remove,keywords_set)
+1567add_dict("see_also",see_also_add,see_also_remove)
+1568
+1569ifcommentisnotNone:
+1570ret["comment"]={"comment":comment}
+1571ifcomment_private:
+1572ret["comment"]["is_private"]=comment_private
+1573
+1574ret.update(rhbzret)
+1575returnret
+
+
+
+
Returns a python dict() with properly formatted parameters to
+pass to update_bugs(). See bugzilla documentation for the format
+of the individual fields:
1582defattachfile(self,idlist,attachfile,description,**kwargs):
+1583"""
+1584 Attach a file to the given bug IDs. Returns the ID of the attachment
+1585 or raises XMLRPC Fault if something goes wrong.
+1586
+1587 attachfile may be a filename (which will be opened) or a file-like
+1588 object, which must provide a 'read' method. If it's not one of these,
+1589 this method will raise a TypeError.
+1590 description is the short description of this attachment.
+1591
+1592 Optional keyword args are as follows:
+1593 file_name: this will be used as the filename for the attachment.
+1594 REQUIRED if attachfile is a file-like object with no
+1595 'name' attribute, otherwise the filename or .name
+1596 attribute will be used.
+1597 comment: An optional comment about this attachment.
+1598 is_private: Set to True if the attachment should be marked private.
+1599 is_patch: Set to True if the attachment is a patch.
+1600 content_type: The mime-type of the attached file. Defaults to
+1601 application/octet-stream if not set. NOTE that text
+1602 files will *not* be viewable in bugzilla unless you
+1603 remember to set this to text/plain. So remember that!
+1604
+1605 Returns the list of attachment ids that were added. If only one
+1606 attachment was added, we return the single int ID for back compat
+1607 """
+1608ifisinstance(attachfile,str):
+1609f=open(attachfile,"rb")
+1610elifhasattr(attachfile,'read'):
+1611f=attachfile
+1612else:
+1613raiseTypeError("attachfile must be filename or file-like object")
+1614
+1615# Back compat
+1616if"contenttype"inkwargs:
+1617kwargs["content_type"]=kwargs.pop("contenttype")
+1618if"ispatch"inkwargs:
+1619kwargs["is_patch"]=kwargs.pop("ispatch")
+1620if"isprivate"inkwargs:
+1621kwargs["is_private"]=kwargs.pop("isprivate")
+1622if"filename"inkwargs:
+1623kwargs["file_name"]=kwargs.pop("filename")
+1624
+1625kwargs['summary']=description
+1626
+1627data=f.read()
+1628ifnotisinstance(data,bytes):# pragma: no cover
+1629data=data.encode(locale.getpreferredencoding())
+1630
+1631if'file_name'notinkwargsandhasattr(f,"name"):
+1632kwargs['file_name']=os.path.basename(f.name)
+1633if'content_type'notinkwargs:
+1634ctype=None
+1635ifkwargs['file_name']:
+1636ctype=mimetypes.guess_type(
+1637kwargs['file_name'],strict=False)[0]
+1638kwargs['content_type']=ctypeor'application/octet-stream'
+1639
+1640ret=self._backend.bug_attachment_create(
+1641listify(idlist),data,kwargs)
+1642
+1643if"attachments"inret:
+1644# Up to BZ 4.2
+1645ret=[int(k)forkinret["attachments"].keys()]
+1646elif"ids"inret:
+1647# BZ 4.4+
+1648ret=ret["ids"]
+1649
+1650ifisinstance(ret,list)andlen(ret)==1:
+1651ret=ret[0]
+1652returnret
+
+
+
+
Attach a file to the given bug IDs. Returns the ID of the attachment
+or raises XMLRPC Fault if something goes wrong.
+
+
attachfile may be a filename (which will be opened) or a file-like
+object, which must provide a 'read' method. If it's not one of these,
+this method will raise a TypeError.
+description is the short description of this attachment.
+
+
Optional keyword args are as follows:
+ file_name: this will be used as the filename for the attachment.
+ REQUIRED if attachfile is a file-like object with no
+ 'name' attribute, otherwise the filename or .name
+ attribute will be used.
+ comment: An optional comment about this attachment.
+ is_private: Set to True if the attachment should be marked private.
+ is_patch: Set to True if the attachment is a patch.
+ content_type: The mime-type of the attached file. Defaults to
+ application/octet-stream if not set. NOTE that text
+ files will not be viewable in bugzilla unless you
+ remember to set this to text/plain. So remember that!
+
+
Returns the list of attachment ids that were added. If only one
+attachment was added, we return the single int ID for back compat
1654defopenattachment_data(self,attachment_dict):
+1655"""
+1656 Helper for turning passed API attachment dictionary into a
+1657 filelike object
+1658 """
+1659ret=BytesIO()
+1660data=attachment_dict["data"]
+1661
+1662ifhasattr(data,"data"):
+1663# This is for xmlrpc Binary
+1664content=data.data# pragma: no cover
+1665else:
+1666importbase64
+1667content=base64.b64decode(data)
+1668
+1669ret.write(content)
+1670ret.name=attachment_dict["file_name"]
+1671ret.seek(0)
+1672returnret
+
+
+
+
Helper for turning passed API attachment dictionary into a
+filelike object
+
+
+
+
+
+
+
+
+ def
+ openattachment(self, attachid):
+
+
+
+
+
+
1674defopenattachment(self,attachid):
+1675"""
+1676 Get the contents of the attachment with the given attachment ID.
+1677 Returns a file-like object.
+1678 """
+1679attachments=self.get_attachments(None,attachid)
+1680data=attachments["attachments"][str(attachid)]
+1681returnself.openattachment_data(data)
+
+
+
+
Get the contents of the attachment with the given attachment ID.
+Returns a file-like object.
1683defupdateattachmentflags(self,bugid,attachid,flagname,**kwargs):
+1684"""
+1685 Updates a flag for the given attachment ID.
+1686 Optional keyword args are:
+1687 status: new status for the flag ('-', '+', '?', 'X')
+1688 requestee: new requestee for the flag
+1689 """
+1690# Bug ID was used for the original custom redhat API, no longer
+1691# needed though
+1692ignore=bugid
+1693
+1694flags={"name":flagname}
+1695flags.update(kwargs)
+1696attachment_ids=[int(attachid)]
+1697update={'flags':[flags]}
+1698
+1699returnself._backend.bug_attachment_update(attachment_ids,update)
+
+
+
+
Updates a flag for the given attachment ID.
+Optional keyword args are:
+ status: new status for the flag ('-', '+', '?', 'X')
+ requestee: new requestee for the flag
1701defget_attachments(self,ids,attachment_ids,
+1702include_fields=None,exclude_fields=None):
+1703"""
+1704 Wrapper for Bug.attachments. One of ids or attachment_ids is required
+1705
+1706 :param ids: Get attachments for this bug ID
+1707 :param attachment_ids: Specific attachment ID to get
+1708
+1709 https://bugzilla.readthedocs.io/en/latest/api/core/v1/attachment.html#get-attachment
+1710 """
+1711params={}
+1712ifinclude_fields:
+1713params["include_fields"]=listify(include_fields)
+1714ifexclude_fields:
+1715params["exclude_fields"]=listify(exclude_fields)
+1716
+1717ifattachment_ids:
+1718returnself._backend.bug_attachment_get(attachment_ids,params)
+1719returnself._backend.bug_attachment_get_all(ids,params)
+
+
+
+
Wrapper for Bug.attachments. One of ids or attachment_ids is required
1729defbuild_createbug(self,
+1730product=None,
+1731component=None,
+1732version=None,
+1733summary=None,
+1734description=None,
+1735comment_private=None,
+1736blocks=None,
+1737cc=None,
+1738assigned_to=None,
+1739keywords=None,
+1740depends_on=None,
+1741groups=None,
+1742op_sys=None,
+1743platform=None,
+1744priority=None,
+1745qa_contact=None,
+1746resolution=None,
+1747severity=None,
+1748status=None,
+1749target_milestone=None,
+1750target_release=None,
+1751url=None,
+1752sub_component=None,
+1753alias=None,
+1754comment_tags=None):
+1755"""
+1756 Returns a python dict() with properly formatted parameters to
+1757 pass to createbug(). See bugzilla documentation for the format
+1758 of the individual fields:
+1759
+1760 https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#update-bug
+1761 """
+1762
+1763localdict={}
+1764ifblocks:
+1765localdict["blocks"]=listify(blocks)
+1766ifcc:
+1767localdict["cc"]=listify(cc)
+1768ifdepends_on:
+1769localdict["depends_on"]=listify(depends_on)
+1770ifgroupsisnotNone:
+1771localdict["groups"]=listify(groups)
+1772ifkeywords:
+1773localdict["keywords"]=listify(keywords)
+1774ifdescription:
+1775localdict["description"]=description
+1776ifcomment_private:
+1777localdict["comment_is_private"]=True
+1778
+1779# Most of the machinery and formatting here is the same as
+1780# build_update, so reuse that as much as possible
+1781ret=self.build_update(product=product,component=component,
+1782version=version,summary=summary,op_sys=op_sys,
+1783platform=platform,priority=priority,qa_contact=qa_contact,
+1784resolution=resolution,severity=severity,status=status,
+1785target_milestone=target_milestone,
+1786target_release=target_release,url=url,
+1787assigned_to=assigned_to,sub_component=sub_component,
+1788alias=alias,comment_tags=comment_tags)
+1789
+1790ret.update(localdict)
+1791returnret
+
+
+
+
Returns a python dict() with properly formatted parameters to
+pass to createbug(). See bugzilla documentation for the format
+of the individual fields:
1824defcreatebug(self,*args,**kwargs):
+1825"""
+1826 Create a bug with the given info. Returns a new Bug object.
+1827 Check bugzilla API documentation for valid values, at least
+1828 product, component, summary, version, and description need to
+1829 be passed.
+1830 """
+1831data=self._validate_createbug(*args,**kwargs)
+1832rawbug=self._backend.bug_create(data)
+1833returnBug(self,bug_id=rawbug["id"],
+1834autorefresh=self.bug_autorefresh)
+
+
+
+
Create a bug with the given info. Returns a new Bug object.
+Check bugzilla API documentation for valid values, at least
+product, component, summary, version, and description need to
+be passed.
+
+
+
+
+
+
+
+
+ def
+ getuser(self, username):
+
+
+
+
+
+
1841defgetuser(self,username):
+1842"""
+1843 Return a bugzilla User for the given username
+1844
+1845 :arg username: The username used in bugzilla.
+1846 :raises XMLRPC Fault: Code 51 if the username does not exist
+1847 :returns: User record for the username
+1848 """
+1849ret=self.getusers(username)
+1850returnretandret[0]
+
+
+
+
Return a bugzilla User for the given username
+
+
:arg username: The username used in bugzilla.
+
+
Raises
+
+
+
XMLRPC Fault: Code 51 if the username does not exist
+:returns: User record for the username
+
+
+
+
+
+
+
+
+
+ def
+ getusers(self, userlist):
+
+
+
+
+
+
1852defgetusers(self,userlist):
+1853"""
+1854 Return a list of Users from .
+1855
+1856 :userlist: List of usernames to lookup
+1857 :returns: List of User records
+1858 """
+1859userlist=listify(userlist)
+1860rawusers=self._backend.user_get({"names":userlist})
+1861userobjs=[User(self,**rawuser)forrawuserin
+1862rawusers.get('users',[])]
+1863
+1864# Return users in same order they were passed in
+1865ret=[]
+1866foruinuserlist:
+1867foruobjinuserobjs[:]:
+1868ifuobj.email==u:
+1869userobjs.remove(uobj)
+1870ret.append(uobj)
+1871break
+1872ret+=userobjs
+1873returnret
+
+
+
+
Return a list of Users from .
+
+
:userlist: List of usernames to lookup
+:returns: List of User records
+
+
+
+
+
+
+
+
+ def
+ searchusers(self, pattern):
+
+
+
+
+
+
1876defsearchusers(self,pattern):
+1877"""
+1878 Return a bugzilla User for the given list of patterns
+1879
+1880 :arg pattern: List of patterns to match against.
+1881 :returns: List of User records
+1882 """
+1883rawusers=self._backend.user_get({"match":listify(pattern)})
+1884return[User(self,**rawuser)forrawuserin
+1885rawusers.get('users',[])]
+
+
+
+
Return a bugzilla User for the given list of patterns
+
+
:arg pattern: List of patterns to match against.
+:returns: List of User records
1887defcreateuser(self,email,name='',password=''):
+1888"""
+1889 Return a bugzilla User for the given username
+1890
+1891 :arg email: The email address to use in bugzilla
+1892 :kwarg name: Real name to associate with the account
+1893 :kwarg password: Password to set for the bugzilla account
+1894 :raises XMLRPC Fault: Code 501 if the username already exists
+1895 Code 500 if the email address isn't valid
+1896 Code 502 if the password is too short
+1897 Code 503 if the password is too long
+1898 :return: User record for the username
+1899 """
+1900args={"email":email}
+1901ifname:
+1902args["name"]=name
+1903ifpassword:
+1904args["password"]=password
+1905self._backend.user_create(args)
+1906returnself.getuser(email)
+
+
+
+
Return a bugzilla User for the given username
+
+
:arg email: The email address to use in bugzilla
+:kwarg name: Real name to associate with the account
+:kwarg password: Password to set for the bugzilla account
+
+
Raises
+
+
+
XMLRPC Fault: Code 501 if the username already exists
+Code 500 if the email address isn't valid
+Code 502 if the password is too short
+Code 503 if the password is too long
1908defupdateperms(self,user,action,groups):
+1909"""
+1910 A method to update the permissions (group membership) of a bugzilla
+1911 user.
+1912
+1913 :arg user: The e-mail address of the user to be acted upon. Can
+1914 also be a list of emails.
+1915 :arg action: add, remove, or set
+1916 :arg groups: list of groups to be added to (i.e. ['fedora_contrib'])
+1917 """
+1918groups=listify(groups)
+1919ifaction=="rem":
+1920action="remove"
+1921ifactionnotin["add","remove","set"]:
+1922raiseBugzillaError("Unknown user permission action '%s'"%action)
+1923
+1924update={
+1925"names":listify(user),
+1926"groups":{
+1927action:groups,
+1928}
+1929}
+1930
+1931returnself._backend.user_update(update)
+
+
+
+
A method to update the permissions (group membership) of a bugzilla
+user.
+
+
:arg user: The e-mail address of the user to be acted upon. Can
+ also be a list of emails.
+:arg action: add, remove, or set
+:arg groups: list of groups to be added to (i.e. ['fedora_contrib'])
1958defgetgroup(self,name,membership=False):
+1959"""
+1960 Return a bugzilla Group for the given name
+1961
+1962 :arg name: The group name used in bugzilla.
+1963 :raises XMLRPC Fault: Code 51 if the name does not exist
+1964 :raises XMLRPC Fault: Code 805 if the user does not have enough
+1965 permissions to view groups
+1966 :returns: Group record for the name
+1967 """
+1968ret=self.getgroups(name,membership=membership)
+1969returnretandret[0]
+
+
+
+
Return a bugzilla Group for the given name
+
+
:arg name: The group name used in bugzilla.
+
+
Raises
+
+
+
XMLRPC Fault: Code 51 if the name does not exist
+
XMLRPC Fault: Code 805 if the user does not have enough
+permissions to view groups
+:returns: Group record for the name
1971defgetgroups(self,grouplist,membership=False):
+1972"""
+1973 Return a list of Groups from .
+1974
+1975 :userlist: List of group names to lookup
+1976 :returns: List of Group records
+1977 """
+1978grouplist=listify(grouplist)
+1979groupobjs=[
+1980Group(self,**rawgroup)
+1981forrawgroupinself._getgroups(
+1982names=grouplist,membership=membership).get('groups',[])
+1983]
+1984
+1985# Return in same order they were passed in
+1986ret=[]
+1987forgingrouplist:
+1988forgobjingroupobjs[:]:
+1989ifgobj.name==g:
+1990groupobjs.remove(gobj)
+1991ret.append(gobj)
+1992break
+1993ret+=groupobjs
+1994returnret
+
+
+
+
Return a list of Groups from .
+
+
:userlist: List of group names to lookup
+:returns: List of Group records
2001defadd_external_tracker(self,bug_ids,ext_bz_bug_id,ext_type_id=None,
+2002ext_type_description=None,ext_type_url=None,
+2003ext_status=None,ext_description=None,
+2004ext_priority=None):
+2005"""
+2006 Wrapper method to allow adding of external tracking bugs using the
+2007 ExternalBugs::WebService::add_external_bug method.
+2008
+2009 This is documented at
+2010 https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#add-external-bug
+2011
+2012 bug_ids: A single bug id or list of bug ids to have external trackers
+2013 added.
+2014 ext_bz_bug_id: The external bug id (ie: the bug number in the
+2015 external tracker).
+2016 ext_type_id: The external tracker id as used by Bugzilla.
+2017 ext_type_description: The external tracker description as used by
+2018 Bugzilla.
+2019 ext_type_url: The external tracker url as used by Bugzilla.
+2020 ext_status: The status of the external bug.
+2021 ext_description: The description of the external bug.
+2022 ext_priority: The priority of the external bug.
+2023 """
+2024param_dict={'ext_bz_bug_id':ext_bz_bug_id}
+2025ifext_type_idisnotNone:
+2026param_dict['ext_type_id']=ext_type_id
+2027ifext_type_descriptionisnotNone:
+2028param_dict['ext_type_description']=ext_type_description
+2029ifext_type_urlisnotNone:
+2030param_dict['ext_type_url']=ext_type_url
+2031ifext_statusisnotNone:
+2032param_dict['ext_status']=ext_status
+2033ifext_descriptionisnotNone:
+2034param_dict['ext_description']=ext_description
+2035ifext_priorityisnotNone:
+2036param_dict['ext_priority']=ext_priority
+2037params={
+2038'bug_ids':listify(bug_ids),
+2039'external_bugs':[param_dict],
+2040}
+2041returnself._backend.externalbugs_add(params)
+
+
+
+
Wrapper method to allow adding of external tracking bugs using the
+ExternalBugs::WebService::add_external_bug method.
bug_ids: A single bug id or list of bug ids to have external trackers
+ added.
+ext_bz_bug_id: The external bug id (ie: the bug number in the
+ external tracker).
+ext_type_id: The external tracker id as used by Bugzilla.
+ext_type_description: The external tracker description as used by
+ Bugzilla.
+ext_type_url: The external tracker url as used by Bugzilla.
+ext_status: The status of the external bug.
+ext_description: The description of the external bug.
+ext_priority: The priority of the external bug.
2043defupdate_external_tracker(self,ids=None,ext_type_id=None,
+2044ext_type_description=None,ext_type_url=None,
+2045ext_bz_bug_id=None,bug_ids=None,
+2046ext_status=None,ext_description=None,
+2047ext_priority=None):
+2048"""
+2049 Wrapper method to allow adding of external tracking bugs using the
+2050 ExternalBugs::WebService::update_external_bug method.
+2051
+2052 This is documented at
+2053 https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#update-external-bug
+2054
+2055 ids: A single external tracker bug id or list of external tracker bug
+2056 ids.
+2057 ext_type_id: The external tracker id as used by Bugzilla.
+2058 ext_type_description: The external tracker description as used by
+2059 Bugzilla.
+2060 ext_type_url: The external tracker url as used by Bugzilla.
+2061 ext_bz_bug_id: A single external bug id or list of external bug ids
+2062 (ie: the bug number in the external tracker).
+2063 bug_ids: A single bug id or list of bug ids to have external tracker
+2064 info updated.
+2065 ext_status: The status of the external bug.
+2066 ext_description: The description of the external bug.
+2067 ext_priority: The priority of the external bug.
+2068 """
+2069params={}
+2070ifidsisnotNone:
+2071params['ids']=listify(ids)
+2072ifext_type_idisnotNone:
+2073params['ext_type_id']=ext_type_id
+2074ifext_type_descriptionisnotNone:
+2075params['ext_type_description']=ext_type_description
+2076ifext_type_urlisnotNone:
+2077params['ext_type_url']=ext_type_url
+2078ifext_bz_bug_idisnotNone:
+2079params['ext_bz_bug_id']=listify(ext_bz_bug_id)
+2080ifbug_idsisnotNone:
+2081params['bug_ids']=listify(bug_ids)
+2082ifext_statusisnotNone:
+2083params['ext_status']=ext_status
+2084ifext_descriptionisnotNone:
+2085params['ext_description']=ext_description
+2086ifext_priorityisnotNone:
+2087params['ext_priority']=ext_priority
+2088returnself._backend.externalbugs_update(params)
+
+
+
+
Wrapper method to allow adding of external tracking bugs using the
+ExternalBugs::WebService::update_external_bug method.
ids: A single external tracker bug id or list of external tracker bug
+ ids.
+ext_type_id: The external tracker id as used by Bugzilla.
+ext_type_description: The external tracker description as used by
+ Bugzilla.
+ext_type_url: The external tracker url as used by Bugzilla.
+ext_bz_bug_id: A single external bug id or list of external bug ids
+ (ie: the bug number in the external tracker).
+bug_ids: A single bug id or list of bug ids to have external tracker
+ info updated.
+ext_status: The status of the external bug.
+ext_description: The description of the external bug.
+ext_priority: The priority of the external bug.
2090defremove_external_tracker(self,ids=None,ext_type_id=None,
+2091ext_type_description=None,ext_type_url=None,
+2092ext_bz_bug_id=None,bug_ids=None):
+2093"""
+2094 Wrapper method to allow removal of external tracking bugs using the
+2095 ExternalBugs::WebService::remove_external_bug method.
+2096
+2097 This is documented at
+2098 https://bugzilla.redhat.com/docs/en/html/integrating/api/Bugzilla/Extension/ExternalBugs/WebService.html#remove-external-bug
+2099
+2100 ids: A single external tracker bug id or list of external tracker bug
+2101 ids.
+2102 ext_type_id: The external tracker id as used by Bugzilla.
+2103 ext_type_description: The external tracker description as used by
+2104 Bugzilla.
+2105 ext_type_url: The external tracker url as used by Bugzilla.
+2106 ext_bz_bug_id: A single external bug id or list of external bug ids
+2107 (ie: the bug number in the external tracker).
+2108 bug_ids: A single bug id or list of bug ids to have external tracker
+2109 info updated.
+2110 """
+2111params={}
+2112ifidsisnotNone:
+2113params['ids']=listify(ids)
+2114ifext_type_idisnotNone:
+2115params['ext_type_id']=ext_type_id
+2116ifext_type_descriptionisnotNone:
+2117params['ext_type_description']=ext_type_description
+2118ifext_type_urlisnotNone:
+2119params['ext_type_url']=ext_type_url
+2120ifext_bz_bug_idisnotNone:
+2121params['ext_bz_bug_id']=listify(ext_bz_bug_id)
+2122ifbug_idsisnotNone:
+2123params['bug_ids']=listify(bug_ids)
+2124returnself._backend.externalbugs_remove(params)
+
+
+
+
Wrapper method to allow removal of external tracking bugs using the
+ExternalBugs::WebService::remove_external_bug method.
ids: A single external tracker bug id or list of external tracker bug
+ ids.
+ext_type_id: The external tracker id as used by Bugzilla.
+ext_type_description: The external tracker description as used by
+ Bugzilla.
+ext_type_url: The external tracker url as used by Bugzilla.
+ext_bz_bug_id: A single external bug id or list of external bug ids
+ (ie: the bug number in the external tracker).
+bug_ids: A single bug id or list of bug ids to have external tracker
+ info updated.
+
+
+
+
+
+
+
+ version =
+'3.3.0'
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 00000000..268e93cf
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/docs/search.js b/docs/search.js
new file mode 100644
index 00000000..e9f1f46f
--- /dev/null
+++ b/docs/search.js
@@ -0,0 +1,46 @@
+window.pdocSearch = (function(){
+/** elasticlunr - http://weixsong.github.io * Copyright (C) 2017 Oliver Nightingale * Copyright (C) 2017 Wei Song * MIT Licensed */!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();o
If you have previously logged into that URL, and have cached login\ntokens, you will automatically be logged in. Otherwise to\nlog in, you can either pass auth options to __init__, or call a login\nhelper like interactive_login().
\n\n
If you are not logged in, you won't be able to access restricted data like\nuser email, or perform write actions like bug create/update. But simple\nquerys will work correctly.
\n\n
If you are unsure if you are logged in, you can check the .logged_in\nproperty.
\n\n
Another way to specify auth credentials is via a 'bugzillarc' file.\nSee readconfig() documentation for details.
If you have previously logged into that URL, and have cached login\ntokens, you will automatically be logged in. Otherwise to\nlog in, you can either pass auth options to __init__, or call a login\nhelper like interactive_login().
\n\n
If you are not logged in, you won't be able to access restricted data like\nuser email, or perform write actions like bug create/update. But simple\nquerys will work correctly.
\n\n
If you are unsure if you are logged in, you can check the .logged_in\nproperty.
\n\n
Another way to specify auth credentials is via a 'bugzillarc' file.\nSee readconfig() documentation for details.
If you have previously logged into that URL, and have cached login\ntokens, you will automatically be logged in. Otherwise to\nlog in, you can either pass auth options to __init__, or call a login\nhelper like interactive_login().
\n\n
If you are not logged in, you won't be able to access restricted data like\nuser email, or perform write actions like bug create/update. But simple\nquerys will work correctly.
\n\n
If you are unsure if you are logged in, you can check the .logged_in\nproperty.
\n\n
Another way to specify auth credentials is via a 'bugzillarc' file.\nSee readconfig() documentation for details.
If you have previously logged into that URL, and have cached login\ntokens, you will automatically be logged in. Otherwise to\nlog in, you can either pass auth options to __init__, or call a login\nhelper like interactive_login().
\n\n
If you are not logged in, you won't be able to access restricted data like\nuser email, or perform write actions like bug create/update. But simple\nquerys will work correctly.
\n\n
If you are unsure if you are logged in, you can check the .logged_in\nproperty.
\n\n
Another way to specify auth credentials is via a 'bugzillarc' file.\nSee readconfig() documentation for details.
If you have previously logged into that URL, and have cached login\ntokens, you will automatically be logged in. Otherwise to\nlog in, you can either pass auth options to __init__, or call a login\nhelper like interactive_login().
\n\n
If you are not logged in, you won't be able to access restricted data like\nuser email, or perform write actions like bug create/update. But simple\nquerys will work correctly.
\n\n
If you are unsure if you are logged in, you can check the .logged_in\nproperty.
\n\n
Another way to specify auth credentials is via a 'bugzillarc' file.\nSee readconfig() documentation for details.
If you have previously logged into that URL, and have cached login\ntokens, you will automatically be logged in. Otherwise to\nlog in, you can either pass auth options to __init__, or call a login\nhelper like interactive_login().
\n\n
If you are not logged in, you won't be able to access restricted data like\nuser email, or perform write actions like bug create/update. But simple\nquerys will work correctly.
\n\n
If you are unsure if you are logged in, you can check the .logged_in\nproperty.
\n\n
Another way to specify auth credentials is via a 'bugzillarc' file.\nSee readconfig() documentation for details.
If you have previously logged into that URL, and have cached login\ntokens, you will automatically be logged in. Otherwise to\nlog in, you can either pass auth options to __init__, or call a login\nhelper like interactive_login().
\n\n
If you are not logged in, you won't be able to access restricted data like\nuser email, or perform write actions like bug create/update. But simple\nquerys will work correctly.
\n\n
If you are unsure if you are logged in, you can check the .logged_in\nproperty.
\n\n
Another way to specify auth credentials is via a 'bugzillarc' file.\nSee readconfig() documentation for details.
If you have previously logged into that URL, and have cached login\ntokens, you will automatically be logged in. Otherwise to\nlog in, you can either pass auth options to __init__, or call a login\nhelper like interactive_login().
\n\n
If you are not logged in, you won't be able to access restricted data like\nuser email, or perform write actions like bug create/update. But simple\nquerys will work correctly.
\n\n
If you are unsure if you are logged in, you can check the .logged_in\nproperty.
\n\n
Another way to specify auth credentials is via a 'bugzillarc' file.\nSee readconfig() documentation for details.
Helper class for historical bugzilla.redhat.com back compat
\n\n
Historically this class used many more non-upstream methods, but\nin 2012 RH started dropping most of its custom bits. By that time,\nupstream BZ had most of the important functionality.
\n\n
Much of the remaining code here is just trying to keep things operating\nin python-bugzilla back compatible manner.
Helper class for historical bugzilla.redhat.com back compat
\n\n
Historically this class used many more non-upstream methods, but\nin 2012 RH started dropping most of its custom bits. By that time,\nupstream BZ had most of the important functionality.
\n\n
Much of the remaining code here is just trying to keep things operating\nin python-bugzilla back compatible manner.
Helper class for historical bugzilla.redhat.com back compat
\n\n
Historically this class used many more non-upstream methods, but\nin 2012 RH started dropping most of its custom bits. By that time,\nupstream BZ had most of the important functionality.
\n\n
Much of the remaining code here is just trying to keep things operating\nin python-bugzilla back compatible manner.
If you have previously logged into that URL, and have cached login\ntokens, you will automatically be logged in. Otherwise to\nlog in, you can either pass auth options to __init__, or call a login\nhelper like interactive_login().
\n\n
If you are not logged in, you won't be able to access restricted data like\nuser email, or perform write actions like bug create/update. But simple\nquerys will work correctly.
\n\n
If you are unsure if you are logged in, you can check the .logged_in\nproperty.
\n\n
Another way to specify auth credentials is via a 'bugzillarc' file.\nSee readconfig() documentation for details.
url: The bugzilla instance URL, which we will connect\nto immediately. Most users will want to specify this at\n__init__ time, but you can defer connecting by passing\nurl=None and calling connect(URL) manually
\n
user: optional username to connect with
\n
password: optional password for the connecting user
\n
cert: optional certificate file for client side certificate\nauthentication
\n
cookiefile: Deprecated, raises an error if not -1 or None
\n
sslverify: Set this to False to skip SSL hostname and CA\nvalidation checks, like out of date certificate
\n
tokenfile: Location to cache the API login token so youi\ndon't have to keep specifying username/password.\nIf -1, use the default path. If None, don't use\nor save any tokenfile.
\n
use_creds: If False, this disables tokenfile\nand configpaths by default. This is a convenience option to\nunset those values at init time. If those values are later\nchanged, they may be used for future operations.
\n
sslverify: Maps to 'requests' sslverify parameter. Set to\nFalse to disable SSL verification, but it can also be a path\nto file or directory for custom certs.
\n
api_key: A bugzilla5+ API key
\n
configpaths: A list of possible bugzillarc locations.
\n
force_rest: Force use of the REST API
\n
force_xmlrpc: Force use of the XMLRPC API. If neither force_X\nparameter are specified, heuristics will be used to determine\nwhich API to use, with XMLRPC preferred for back compatability.
\n
requests_session: An optional requests.Session object the\nAPI will use to contact the remote bugzilla instance. This\nway the API user can set up whatever auth bits they may need.
configpath: Optional bugzillarc path to read, instead of\nthe default list.
\n
\n\n
This function is called automatically from Bugzilla connect(), which\nis called at __init__ if a URL is passed. Calling it manually is\njust for passing in a non-standard configpath.
\n\n
The locations for the bugzillarc file are preferred in this order:
It has content like:\n [bugzilla.yoursite.com]\n user = username\n password = password\nOr\n [bugzilla.yoursite.com]\n api_key = key
\n\n
The file can have multiple sections for different bugzilla instances.\nA 'url' field in the [DEFAULT] section can be used to set a default\nURL for the bugzilla command line tool.
\n\n
Be sure to set appropriate permissions on bugzillarc if you choose to\nstore your password in it!
\n\n
\n
overwrite: If True, bugzillarc will clobber any already\nset self.user/password/api_key/cert value.
Connect to the bugzilla instance with the given url. This is\ncalled by __init__ if a URL is passed. Or it can be called manually\nat any time with a passed URL.
\n\n
This will also read any available config files (see readconfig()),\nwhich may set 'user' and 'password', and others.
\n\n
If 'user' and 'password' are both set, we'll run login(). Otherwise\nyou'll have to login() yourself before some methods will work.
Attempt to log in using the given username and password. Subsequent\nmethod calls will use this username and password. Returns False if\nlogin fails, otherwise returns some kind of login info - typically\neither a numeric userid, or a dict of user info.
\n\n
If user is not set, the value of Bugzilla.user will be used. If that\nis not set, ValueError will be raised. If login fails, BugzillaError\nwill be raised.
\n\n
The login session can be restricted to current user IP address\nwith restrict_login argument. (Bugzilla 4.4+)
\n\n
This method will be called implicitly at the end of connect() if user\nand password are both set. So under most circumstances you won't need\nto call this yourself.
This is True if this instance is logged in else False.
\n\n
We test if this session is authenticated by calling the User.get()\nXMLRPC method with ids set. Logged-out users cannot pass the 'ids'\nparameter and will result in a 505 error. If we tried to login with a\ntoken, but the token was incorrect or expired, the server returns a\n32000 error.
\n\n
For Bugzilla 5 and later, a new method, User.valid_login is available\nto test the validity of the token. However, this will require that the\nusername be cached along with the token in order to work effectively in\nall scenarios and is not currently used. For more information, refer to\nthe following url.
Calls getBugFields, which returns a list of fields in each bug\nfor this bugzilla instance. This can be used to set the list of attrs\non the Bug object.
\n\n
Parameters
\n\n
\n
force_refresh: If True, overwrite the bugfield cache\nwith these newly checked values.
This does not perform any caching like other product API calls.\nIf ids, names, or ptype is not specified, we default to\nptype=accessible for historical reasons
\n\n
@ids: List of product IDs to lookup\n@names: List of product names to lookup\n@ptype: Either 'accessible', 'selectable', or 'enterable'. If\n specified, we return data for all those\n@include_fields: Only include these fields in the output\n@exclude_fields: Do not include these fields in the output
Refresh a product's cached info. Basically calls product_get\nwith the passed arguments, and tries to intelligently update\nour product cache.
\n\n
For example, if we already have cached info for product=foo,\nand you pass in names=[\"bar\", \"baz\"], the new cache will have\ninfo for products foo, bar, baz. Individual product fields are\nalso updated.
Query all products and return the raw dict info. Takes all the\nsame arguments as product_get.
\n\n
On first invocation this will contact bugzilla and internally\ncache the results. Subsequent getproducts calls or accesses to\nself.products will return this cached data only.
\n\n
Parameters
\n\n
\n
force_refresh: force refreshing via refresh_products()
Wrapper around Product.get(include_fields=[\"components\"]),\nreturning only the \"components\" data for the requested product,\nslightly reworked to a dict mapping of components.name: components,\nfor historical reasons.
\n\n
This uses the product cache, but will update it if the product\nisn't found or \"components\" isn't cached for the product.
\n\n
In cases like bugzilla.redhat.com where there are tons of\ncomponents for some products, this API will time out. You\nshould use product_get instead.
A method to create a component in Bugzilla. Takes a dict, with the\nfollowing elements:
\n\n
product: The product to create the component in\ncomponent: The name of the component to create\ndescription: A one sentence summary of the component\ndefault_assignee: The bugzilla login (email address) of the initial\n owner of the component\ndefault_qa_contact (optional): The bugzilla login of the\n initial QA contact\ndefault_cc: (optional) The initial list of users to be CC'ed on\n new bugs for the component.\nis_active: (optional) If False, the component is hidden from\n the component list when filing new bugs.
A method to edit a component in Bugzilla. Takes a dict, with\nmandatory elements of product. component, and initialowner.\nAll other elements are optional and use the same names as the\naddcomponent() method.
This value is passed to Bug.autorefresh for all fetched bugs.\nIf True, and an uncached attribute is requested from a Bug,\n the Bug will update its contents and try again.
Return a list of Bug objects with the full complement of bug data\nalready loaded. If there's a problem getting the data for a given id,\nthe corresponding item in the returned list will be None.
Build a query string from passed arguments. Will handle\nquery parameter differences between various bugzilla versions.
\n\n
Most of the parameters should be self-explanatory. However,\nif you want to perform a complex query, and easy way is to\ncreate it with the bugzilla web UI, copy the entire URL it\ngenerates, and pass it to the static method
Same as query(), but the return value is altered to be\n(buglist, values), where values is raw dictionary output from\nthe API call, excluding the bug content. For example this may\ninclude a limit value if the bugzilla instance puts an implied\nlimit on returned result numbers.
Returns a python dict() with properly formatted parameters to\npass to update_bugs(). See bugzilla documentation for the format\nof the individual fields:
Attach a file to the given bug IDs. Returns the ID of the attachment\nor raises XMLRPC Fault if something goes wrong.
\n\n
attachfile may be a filename (which will be opened) or a file-like\nobject, which must provide a 'read' method. If it's not one of these,\nthis method will raise a TypeError.\ndescription is the short description of this attachment.
\n\n
Optional keyword args are as follows:\n file_name: this will be used as the filename for the attachment.\n REQUIRED if attachfile is a file-like object with no\n 'name' attribute, otherwise the filename or .name\n attribute will be used.\n comment: An optional comment about this attachment.\n is_private: Set to True if the attachment should be marked private.\n is_patch: Set to True if the attachment is a patch.\n content_type: The mime-type of the attached file. Defaults to\n application/octet-stream if not set. NOTE that text\n files will not be viewable in bugzilla unless you\n remember to set this to text/plain. So remember that!
\n\n
Returns the list of attachment ids that were added. If only one\nattachment was added, we return the single int ID for back compat
Updates a flag for the given attachment ID.\nOptional keyword args are:\n status: new status for the flag ('-', '+', '?', 'X')\n requestee: new requestee for the flag
Returns a python dict() with properly formatted parameters to\npass to createbug(). See bugzilla documentation for the format\nof the individual fields:
Create a bug with the given info. Returns a new Bug object.\nCheck bugzilla API documentation for valid values, at least\nproduct, component, summary, version, and description need to\nbe passed.
:arg email: The email address to use in bugzilla\n:kwarg name: Real name to associate with the account\n:kwarg password: Password to set for the bugzilla account
\n\n
Raises
\n\n
\n
XMLRPC Fault: Code 501 if the username already exists\nCode 500 if the email address isn't valid\nCode 502 if the password is too short\nCode 503 if the password is too long
A method to update the permissions (group membership) of a bugzilla\nuser.
\n\n
:arg user: The e-mail address of the user to be acted upon. Can\n also be a list of emails.\n:arg action: add, remove, or set\n:arg groups: list of groups to be added to (i.e. ['fedora_contrib'])
bug_ids: A single bug id or list of bug ids to have external trackers\n added.\next_bz_bug_id: The external bug id (ie: the bug number in the\n external tracker).\next_type_id: The external tracker id as used by Bugzilla.\next_type_description: The external tracker description as used by\n Bugzilla.\next_type_url: The external tracker url as used by Bugzilla.\next_status: The status of the external bug.\next_description: The description of the external bug.\next_priority: The priority of the external bug.
ids: A single external tracker bug id or list of external tracker bug\n ids.\next_type_id: The external tracker id as used by Bugzilla.\next_type_description: The external tracker description as used by\n Bugzilla.\next_type_url: The external tracker url as used by Bugzilla.\next_bz_bug_id: A single external bug id or list of external bug ids\n (ie: the bug number in the external tracker).\nbug_ids: A single bug id or list of bug ids to have external tracker\n info updated.\next_status: The status of the external bug.\next_description: The description of the external bug.\next_priority: The priority of the external bug.
ids: A single external tracker bug id or list of external tracker bug\n ids.\next_type_id: The external tracker id as used by Bugzilla.\next_type_description: The external tracker description as used by\n Bugzilla.\next_type_url: The external tracker url as used by Bugzilla.\next_bz_bug_id: A single external bug id or list of external bug ids\n (ie: the bug number in the external tracker).\nbug_ids: A single bug id or list of bug ids to have external tracker\n info updated.