Skip to content

Commit d63b664

Browse files
authored
Implement Bugzilla REST API client (fixes #189, fixes #198) (#217)
* Implement Bugzilla REST API client (fixes #189) * Set api_key param in _call() * Add requests in pyproject.toml
1 parent a5ec7f6 commit d63b664

File tree

7 files changed

+519
-354
lines changed

7 files changed

+519
-354
lines changed

jbi/actions/default.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ def create_and_link_issue( # pylint: disable=too-many-locals
263263
extra=log_context.update(operation=Operation.LINK).dict(),
264264
)
265265
bugzilla_response = self.bugzilla_client.update_bug(
266-
bug_obj, see_also_add=jira_url
266+
bug_obj.id, see_also_add=jira_url
267267
)
268268

269269
bugzilla_url = f"{settings.bugzilla_base_url}/show_bug.cgi?id={bug_obj.id}"

jbi/services.py

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
from typing import TYPE_CHECKING
77

88
import backoff
9-
import bugzilla as rh_bugzilla
9+
import requests
1010
from atlassian import Jira, errors
1111
from pydantic import parse_obj_as
1212
from statsd.defaults.env import statsd
1313

1414
from jbi import environment
15-
from jbi.models import BugzillaBug, BugzillaComment
15+
from jbi.models import BugzillaApiResponse, BugzillaBug, BugzillaComment
1616

1717
if TYPE_CHECKING:
1818
from jbi.models import Actions
@@ -86,25 +86,49 @@ def jira_visible_projects(jira=None) -> list[dict]:
8686
return projects
8787

8888

89-
class BugzillaClient:
90-
"""
91-
Wrapper around the Bugzilla client to turn responses into our models instances.
92-
"""
89+
class BugzillaClientError(Exception):
90+
"""Errors raised by `BugzillaClient`."""
91+
9392

94-
def __init__(self, base_url: str, api_key: str):
95-
"""Constructor"""
96-
self._client = rh_bugzilla.Bugzilla(base_url, api_key=api_key)
93+
class BugzillaClient:
94+
"""A wrapper around `requests` to interact with a Bugzilla REST API."""
95+
96+
def __init__(self, base_url, api_key):
97+
"""Initialize the client, without network activity."""
98+
self.base_url = base_url
99+
self.api_key = api_key
100+
self._client = requests.Session()
101+
102+
def _call(self, verb, url, *args, **kwargs):
103+
"""Send HTTP requests with API key in querystring parameters."""
104+
# Send API key as querystring parameter.
105+
kwargs.setdefault("params", {}).setdefault("api_key", self.api_key)
106+
resp = self._client.request(verb, url, *args, **kwargs)
107+
resp.raise_for_status()
108+
parsed = resp.json()
109+
if parsed.get("error"):
110+
raise BugzillaClientError(parsed["message"])
111+
return parsed
97112

98113
@property
99-
def logged_in(self):
100-
"""Return `true` if credentials are valid"""
101-
return self._client.logged_in
114+
def logged_in(self) -> bool:
115+
"""Verify the API key validity."""
116+
# https://bugzilla.readthedocs.io/en/latest/api/core/v1/user.html#who-am-i
117+
resp = self._call("GET", f"{self.base_url}/rest/whoami")
118+
return "id" in resp
102119

103120
def get_bug(self, bugid) -> BugzillaBug:
104-
"""Return the Bugzilla object with all attributes"""
105-
response = self._client.getbug(bugid).__dict__
106-
bug = BugzillaBug.parse_obj(response)
107-
# If comment is private, then webhook does not have comment, fetch it from server
121+
"""Retrieve details about the specified bug id."""
122+
# https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#rest-single-bug
123+
url = f"{self.base_url}/rest/bug/{bugid}"
124+
bug_info = self._call("GET", url)
125+
parsed = BugzillaApiResponse.parse_obj(bug_info)
126+
if not parsed.bugs:
127+
raise BugzillaClientError(
128+
f"Unexpected response content from 'GET {url}' (no 'bugs' field)"
129+
)
130+
bug = parsed.bugs[0]
131+
# If comment is private, then fetch it from server
108132
if bug.comment and bug.comment.is_private:
109133
comment_list = self.get_comments(bugid)
110134
matching_comments = [c for c in comment_list if c.id == bug.comment.id]
@@ -114,15 +138,28 @@ def get_bug(self, bugid) -> BugzillaBug:
114138
return bug
115139

116140
def get_comments(self, bugid) -> list[BugzillaComment]:
117-
"""Return the list of comments for the specified bug ID"""
118-
response = self._client.get_comments(idlist=[bugid])
119-
comments = response["bugs"][str(bugid)]["comments"]
141+
"""Retrieve the list of comments of the specified bug id."""
142+
# https://bugzilla.readthedocs.io/en/latest/api/core/v1/comment.html#rest-comments
143+
url = f"{self.base_url}/rest/bug/{bugid}/comment"
144+
comments_info = self._call("GET", url)
145+
comments = comments_info.get("bugs", {}).get(str(bugid), {}).get("comments")
146+
if comments is None:
147+
raise BugzillaClientError(
148+
f"Unexpected response content from 'GET {url}' (no 'bugs' field)"
149+
)
120150
return parse_obj_as(list[BugzillaComment], comments)
121151

122-
def update_bug(self, bugid, **attrs):
123-
"""Update the specified bug with the specified attributes"""
124-
update = self._client.build_update(**attrs)
125-
return self._client.update_bugs([bugid], update)
152+
def update_bug(self, bugid, **fields) -> BugzillaBug:
153+
"""Update the specified fields of the specified bug."""
154+
# https://bugzilla.readthedocs.io/en/latest/api/core/v1/bug.html#rest-update-bug
155+
url = f"{self.base_url}/rest/bug/{bugid}"
156+
updated_info = self._call("PUT", url, json=fields)
157+
parsed = BugzillaApiResponse.parse_obj(updated_info)
158+
if not parsed.bugs:
159+
raise BugzillaClientError(
160+
f"Unexpected response content from 'PUT {url}' (no 'bugs' field)"
161+
)
162+
return parsed.bugs[0]
126163

127164

128165
def get_bugzilla():
@@ -139,7 +176,10 @@ def get_bugzilla():
139176
wrapped=bugzilla_client,
140177
prefix="bugzilla",
141178
methods=instrumented_methods,
142-
exceptions=(rh_bugzilla.BugzillaError,),
179+
exceptions=(
180+
BugzillaClientError,
181+
requests.RequestException,
182+
),
143183
)
144184

145185

0 commit comments

Comments
 (0)