Skip to content

Commit 49a865d

Browse files
committed
Finish _BackendREST implementation and Bugzilla probing
The REST API is mostly working. Some test suite bits are skipped, but this mostly seems due to some Red Hat specific APIs that I can't find implementations for. The URL probing logic is: * If URL contains '/xmlrpc', assume XMLRPC * If URL contains '/rest', assume REST * Otherwise try the expected xmlrpc.cgi URL and if it exists, use it * Otherwise tru the expected /rest URL and if it exists, use it * Otherwise just attempt to initialize the XMLRPC backend Signed-off-by: Cole Robinson <[email protected]>
1 parent 5ab1123 commit 49a865d

7 files changed

Lines changed: 277 additions & 98 deletions

File tree

bugzilla/_backendbase.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# This work is licensed under the GNU GPLv2 or later.
22
# See the COPYING file in the top-level directory.
33

4+
from logging import getLogger
5+
6+
import requests
7+
8+
log = getLogger(__name__)
9+
410

511
class _BackendBase(object):
612
"""
@@ -12,6 +18,17 @@ def __init__(self, url, bugzillasession):
1218
self._url = url
1319
self._bugzillasession = bugzillasession
1420

21+
22+
@staticmethod
23+
def probe(url):
24+
try:
25+
requests.head(url).raise_for_status()
26+
return True # pragma: no cover
27+
except Exception as e:
28+
log.debug("Failed to probe url=%s : %s", url, str(e))
29+
return False
30+
31+
1532
#################
1633
# Internal APIs #
1734
#################

bugzilla/_backendrest.py

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
# This work is licensed under the GNU GPLv2 or later.
22
# See the COPYING file in the top-level directory.
33

4+
import base64
45
import json
56
import logging
67
import os
78

89
from ._backendbase import _BackendBase
910
from .exceptions import BugzillaError
11+
from ._util import listify
1012

1113

1214
log = logging.getLogger(__name__)
1315

1416

15-
# XXX remove this pylint disable
16-
# pylint: disable=abstract-method
17+
def _update_key(indict, updict, key):
18+
if key not in indict:
19+
indict[key] = {}
20+
indict[key].update(updict.get(key, {}))
1721

1822

1923
class _BackendREST(_BackendBase):
@@ -78,3 +82,112 @@ def is_rest(self):
7882

7983
def bugzilla_version(self):
8084
return self._get("/version")
85+
86+
def bug_create(self, paramdict):
87+
return self._post("/bug", paramdict)
88+
def bug_fields(self, paramdict):
89+
return self._get("/field/bug", paramdict)
90+
def bug_get(self, bug_ids, aliases, paramdict):
91+
data = paramdict.copy()
92+
data["id"] = listify(bug_ids)
93+
data["alias"] = listify(aliases)
94+
ret = self._get("/bug", data)
95+
return ret
96+
97+
def bug_attachment_get(self, attachment_ids, paramdict):
98+
# XMLRPC supported mutiple fetch at once, but not REST
99+
ret = {}
100+
for attid in listify(attachment_ids):
101+
out = self._get("/bug/attachment/%s" % attid, paramdict)
102+
_update_key(ret, out, "attachments")
103+
_update_key(ret, out, "bugs")
104+
return ret
105+
106+
def bug_attachment_get_all(self, bug_ids, paramdict):
107+
# XMLRPC supported mutiple fetch at once, but not REST
108+
ret = {}
109+
for bugid in listify(bug_ids):
110+
out = self._get("/bug/%s/attachment" % bugid, paramdict)
111+
_update_key(ret, out, "attachments")
112+
_update_key(ret, out, "bugs")
113+
return ret
114+
115+
def bug_attachment_create(self, bug_ids, data, paramdict):
116+
if data is not None and "data" not in paramdict:
117+
paramdict["data"] = base64.b64encode(data).decode("utf-8")
118+
paramdict["ids"] = listify(bug_ids)
119+
return self._post("/bug/%s/attachment" % paramdict["ids"][0],
120+
paramdict)
121+
122+
def bug_attachment_update(self, attachment_ids, paramdict):
123+
paramdict["ids"] = listify(attachment_ids)
124+
return self._put("/bug/attachment/%s" % paramdict["ids"][0], paramdict)
125+
126+
def bug_comments(self, bug_ids, paramdict):
127+
# XMLRPC supported mutiple fetch at once, but not REST
128+
ret = {}
129+
for bugid in bug_ids:
130+
out = self._get("/bug/%s/comment" % bugid, paramdict)
131+
_update_key(ret, out, "bugs")
132+
return ret
133+
def bug_history(self, bug_ids, paramdict):
134+
# XMLRPC supported mutiple fetch at once, but not REST
135+
ret = {"bugs": []}
136+
for bugid in bug_ids:
137+
out = self._get("/bug/%s/history" % bugid, paramdict)
138+
ret["bugs"].extend(out.get("bugs", []))
139+
return ret
140+
141+
def bug_search(self, paramdict):
142+
return self._get("/bug", paramdict)
143+
def bug_update(self, bug_ids, paramdict):
144+
data = paramdict.copy()
145+
data["ids"] = listify(bug_ids)
146+
return self._put("/bug/%s" % data["ids"][0], data)
147+
def bug_update_tags(self, bug_ids, paramdict):
148+
raise BugzillaError("No REST API available for bug_update_tags")
149+
150+
def component_create(self, paramdict):
151+
return self._post("/component", paramdict)
152+
def component_update(self, paramdict):
153+
if "ids" in paramdict:
154+
apiurl = str(listify(paramdict["ids"])[0])
155+
if "names" in paramdict:
156+
apiurl = ("%(product)s/%(component)s" %
157+
listify(paramdict["names"])[0])
158+
return self._put("/component/%s" % apiurl, paramdict)
159+
160+
def externalbugs_add(self, paramdict):
161+
raise BugzillaError(
162+
"No REST API available yet for externalbugs_add")
163+
def externalbugs_remove(self, paramdict):
164+
raise BugzillaError(
165+
"No REST API available yet for externalbugs_remove")
166+
def externalbugs_update(self, paramdict):
167+
raise BugzillaError(
168+
"No REST API available yet for externalbugs_update")
169+
170+
def product_get(self, paramdict):
171+
return self._get("/product/get", paramdict)
172+
def product_get_accessible(self):
173+
return self._get("/product_accessible")
174+
def product_get_enterable(self):
175+
return self._get("/product_enterable")
176+
def product_get_selectable(self):
177+
return self._get("/product_selectable")
178+
179+
def user_create(self, paramdict):
180+
return self._post("/user", paramdict)
181+
def user_get(self, paramdict):
182+
return self._get("/user", paramdict)
183+
def user_login(self, paramdict):
184+
return self._get("/login", paramdict)
185+
def user_logout(self):
186+
return self._get("/logout")
187+
def user_update(self, paramdict):
188+
urlid = None
189+
if "ids" in paramdict:
190+
urlid = listify(paramdict["ids"])[0]
191+
if "names" in paramdict:
192+
urlid = listify(paramdict["names"])[0]
193+
return self._put("/user/%s" % urlid, paramdict)

bugzilla/base.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -454,15 +454,35 @@ def _set_bz_version(self, version):
454454
minor = 0
455455
self._cache.version_parsed = (major, minor)
456456

457-
def _get_backend_class(self): # pragma: no cover
457+
def _get_backend_class(self, url): # pragma: no cover
458458
# This is a hook for the test suite to do some mock hackery
459+
if self._force_rest and self._force_xmlrpc:
460+
raise BugzillaError(
461+
"Cannot specify both force_rest and force_xmlrpc")
462+
463+
xmlurl = self.fix_url(url)
464+
if self._force_xmlrpc:
465+
return _BackendXMLRPC, xmlurl
466+
467+
resturl = self.fix_url(url, force_rest=self._force_rest)
459468
if self._force_rest:
460-
return _BackendREST
461-
elif self._force_xmlrpc:
462-
return _BackendXMLRPC
463-
# default to XMLRPC, like before
464-
# FIXME: guess backend?
465-
return _BackendXMLRPC
469+
return _BackendREST, resturl
470+
471+
# Simple heuristic if the original url has a path in it
472+
if "/xmlrpc" in url:
473+
return _BackendXMLRPC, xmlurl
474+
if "/rest" in url:
475+
return _BackendREST, resturl
476+
477+
# We were passed something like bugzilla.example.com but we
478+
# aren't sure which method to use, try probing
479+
if _BackendXMLRPC.probe(xmlurl):
480+
return _BackendXMLRPC, xmlurl
481+
if _BackendREST.probe(resturl):
482+
return _BackendREST, resturl
483+
484+
# Otherwise fallback to XMLRPC default and let it fail
485+
return _BackendXMLRPC, xmlurl
466486

467487
def connect(self, url=None):
468488
"""
@@ -479,10 +499,7 @@ def connect(self, url=None):
479499
if self._session:
480500
self.disconnect()
481501

482-
if url is None and self.url:
483-
url = self.url
484-
url = self.fix_url(url, force_rest=self._force_rest)
485-
502+
backendclass, url = self._get_backend_class(url or self.url)
486503
self.url = url
487504
# we've changed URLs - reload config
488505
self.readconfig(overwrite=False)
@@ -494,7 +511,6 @@ def connect(self, url=None):
494511
tokencache=self._tokencache,
495512
api_key=self.api_key,
496513
requests_session=self._user_requests_session)
497-
backendclass = self._get_backend_class()
498514
self._backend = backendclass(url, self._session)
499515

500516
if (self.user and self.password):

tests/mockbackend.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,15 @@ def make_bz(bz_kwargs=None, rhbz=False, **kwargs):
132132

133133
if "use_creds" not in bz_kwargs:
134134
bz_kwargs["use_creds"] = False
135+
135136
bz = bugzilla.Bugzilla(url=None, **bz_kwargs)
136137
backendclass = _make_backend_class(**kwargs)
138+
139+
def _get_backend_class(url):
140+
return backendclass, bugzilla.Bugzilla.fix_url(url)
141+
137142
# pylint: disable=protected-access
138-
bz._get_backend_class = lambda *a, **k: backendclass
143+
bz._get_backend_class = _get_backend_class
139144

140145
url = "https:///TESTSUITEMOCK"
141146
if rhbz:

0 commit comments

Comments
 (0)