66from typing import TYPE_CHECKING
77
88import backoff
9- import bugzilla as rh_bugzilla
9+ import requests
1010from atlassian import Jira , errors
1111from pydantic import parse_obj_as
1212from statsd .defaults .env import statsd
1313
1414from jbi import environment
15- from jbi .models import BugzillaBug , BugzillaComment
15+ from jbi .models import BugzillaApiResponse , BugzillaBug , BugzillaComment
1616
1717if 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
128165def 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