6
6
from typing import TYPE_CHECKING
7
7
8
8
import backoff
9
- import bugzilla as rh_bugzilla
9
+ import requests
10
10
from atlassian import Jira , errors
11
11
from pydantic import parse_obj_as
12
12
from statsd .defaults .env import statsd
13
13
14
14
from jbi import environment
15
- from jbi .models import BugzillaBug , BugzillaComment
15
+ from jbi .models import BugzillaApiResponse , BugzillaBug , BugzillaComment
16
16
17
17
if TYPE_CHECKING :
18
18
from jbi .models import Actions
@@ -86,25 +86,49 @@ def jira_visible_projects(jira=None) -> list[dict]:
86
86
return projects
87
87
88
88
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
+
93
92
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
97
112
98
113
@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
102
119
103
120
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
108
132
if bug .comment and bug .comment .is_private :
109
133
comment_list = self .get_comments (bugid )
110
134
matching_comments = [c for c in comment_list if c .id == bug .comment .id ]
@@ -114,15 +138,28 @@ def get_bug(self, bugid) -> BugzillaBug:
114
138
return bug
115
139
116
140
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
+ )
120
150
return parse_obj_as (list [BugzillaComment ], comments )
121
151
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 ]
126
163
127
164
128
165
def get_bugzilla ():
@@ -139,7 +176,10 @@ def get_bugzilla():
139
176
wrapped = bugzilla_client ,
140
177
prefix = "bugzilla" ,
141
178
methods = instrumented_methods ,
142
- exceptions = (rh_bugzilla .BugzillaError ,),
179
+ exceptions = (
180
+ BugzillaClientError ,
181
+ requests .RequestException ,
182
+ ),
143
183
)
144
184
145
185
0 commit comments