Mercurial > hg > bzconsole
annotate bzconsole/api.py @ 41:5d09b4e9a21b
cleanup
author | Jeff Hammel <jhammel@mozilla.com> |
---|---|
date | Sun, 16 Dec 2012 20:04:03 -0800 |
parents | 99cdb343273f |
children | 678b82fedb40 |
rev | line source |
---|---|
20 | 1 """ |
2 console API to bugzilla | |
3 """ | |
4 | |
40 | 5 import base64 |
20 | 6 import httplib |
7 import json | |
8 import os | |
38
0a3c7d8eec72
utilize patch to determine if a patch or not
Jeff Hammel <jhammel@mozilla.com>
parents:
36
diff
changeset
|
9 import patch |
20 | 10 import subprocess |
11 import sys | |
12 import urllib | |
13 import urllib2 | |
14 | |
15 from StringIO import StringIO | |
16 | |
17 from utils import tmpbuffer | |
18 | |
19 | |
20 class BZapi(object): | |
21 """ | |
22 console API to bugzilla | |
23 """ | |
24 | |
35
94b0b7b4f190
attachment filename for bzapi
Jeff Hammel <jhammel@mozilla.com>
parents:
34
diff
changeset
|
25 # currently there is only one cache for configuration |
20 | 26 config_cache = '.bzconfiguration' |
27 | |
28 def __init__(self, | |
29 server='https://api-dev.bugzilla.mozilla.org/latest', | |
30 refresh=False, | |
31 print_request=None, | |
32 username=None, | |
33 password=None): | |
34 """ | |
35 - refresh : refresh the (cached) configuration | |
36 - print_request : log any request information to a file | |
37 """ | |
38 self.server = server | |
39 self.refresh = refresh | |
40 self.print_request = print_request | |
41 self.username = username | |
42 self.password = password | |
43 | |
44 def products(self, classification=None): | |
45 """list bugzilla products""" | |
46 configuration = self.configuration() | |
47 if classification: | |
48 products = [i for i in configuration['product'] if configuration['product'][i]['classification'] == 'Components'] | |
49 return sorted(products) | |
50 else: | |
51 return sorted(configuration['product'].keys()) | |
52 | |
53 def components(self, product): | |
54 """list bugzilla components for a particular product""" | |
55 configuration = self.configuration() | |
56 assert product in configuration['product'], 'Product %s not found' % product | |
57 return sorted(configuration['product'][product]['component'].keys()) | |
58 | |
59 def _unique_components(self): | |
60 """returns a dict of unique component, product""" | |
61 retval = {} | |
62 dupe = set() | |
63 for product in self.products(): | |
64 for component in self.components(product): | |
65 if component in retval: | |
66 dupe.add(component) | |
67 else: | |
68 retval[component] = product | |
69 for d in dupe: | |
70 del retval[d] | |
71 return retval, dupe | |
72 | |
26
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
73 def users(self, match): |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
74 """returns users matching a search string""" |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
75 assert self.username and self.password, "Must be authenticated" |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
76 return self._request('/user?match=%s' % match) |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
77 |
27 | 78 def bug(self, number): |
79 """display a bug""" | |
80 number = int(number) | |
81 return self._request('/bug/%d' % number) | |
82 | |
21
e04729acf5be
allow bzconsole to implicitly get the product from the component for new bugs, if the component is unique
Jeff Hammel <jhammel@mozilla.com>
parents:
20
diff
changeset
|
83 def new(self, component, title, product=None, |
32 | 84 version=None, description=None, whiteboard=(), cc=(), |
85 blocks=(), depends_on=()): | |
20 | 86 """file a new bug. username and password must be set""" |
87 | |
88 # sanity check | |
21
e04729acf5be
allow bzconsole to implicitly get the product from the component for new bugs, if the component is unique
Jeff Hammel <jhammel@mozilla.com>
parents:
20
diff
changeset
|
89 if product: |
28
a46a76a7990d
fix single version products and bump version
Jeff Hammel <jhammel@mozilla.com>
parents:
27
diff
changeset
|
90 assert product in self.products(), "Product not found" |
a46a76a7990d
fix single version products and bump version
Jeff Hammel <jhammel@mozilla.com>
parents:
27
diff
changeset
|
91 assert component in self.components(product), "Component not found" |
21
e04729acf5be
allow bzconsole to implicitly get the product from the component for new bugs, if the component is unique
Jeff Hammel <jhammel@mozilla.com>
parents:
20
diff
changeset
|
92 else: |
28
a46a76a7990d
fix single version products and bump version
Jeff Hammel <jhammel@mozilla.com>
parents:
27
diff
changeset
|
93 unique, dupe = self._unique_components() |
a46a76a7990d
fix single version products and bump version
Jeff Hammel <jhammel@mozilla.com>
parents:
27
diff
changeset
|
94 assert component in unique, 'Unique component not found: %s' % component |
a46a76a7990d
fix single version products and bump version
Jeff Hammel <jhammel@mozilla.com>
parents:
27
diff
changeset
|
95 product = unique[component] |
20 | 96 assert title, 'Must provide a bug summary' |
97 assert self.username and self.password, "Must be authenticated" | |
98 | |
99 # infer version if not given | |
100 if version is None: | |
28
a46a76a7990d
fix single version products and bump version
Jeff Hammel <jhammel@mozilla.com>
parents:
27
diff
changeset
|
101 versions = self._configuration['product'][product]['version'] |
a46a76a7990d
fix single version products and bump version
Jeff Hammel <jhammel@mozilla.com>
parents:
27
diff
changeset
|
102 if len(versions) == 1: |
a46a76a7990d
fix single version products and bump version
Jeff Hammel <jhammel@mozilla.com>
parents:
27
diff
changeset
|
103 version = versions[0] |
a46a76a7990d
fix single version products and bump version
Jeff Hammel <jhammel@mozilla.com>
parents:
27
diff
changeset
|
104 else: |
a46a76a7990d
fix single version products and bump version
Jeff Hammel <jhammel@mozilla.com>
parents:
27
diff
changeset
|
105 default_versions = ('unspecified', 'Trunk') |
a46a76a7990d
fix single version products and bump version
Jeff Hammel <jhammel@mozilla.com>
parents:
27
diff
changeset
|
106 for ver in default_versions: |
a46a76a7990d
fix single version products and bump version
Jeff Hammel <jhammel@mozilla.com>
parents:
27
diff
changeset
|
107 version = ver |
a46a76a7990d
fix single version products and bump version
Jeff Hammel <jhammel@mozilla.com>
parents:
27
diff
changeset
|
108 if version in self._configuration['product'][product]['version']: |
a46a76a7990d
fix single version products and bump version
Jeff Hammel <jhammel@mozilla.com>
parents:
27
diff
changeset
|
109 break |
20 | 110 assert version in self._configuration['product'][product]['version'], 'Version not found' |
21
e04729acf5be
allow bzconsole to implicitly get the product from the component for new bugs, if the component is unique
Jeff Hammel <jhammel@mozilla.com>
parents:
20
diff
changeset
|
111 |
26
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
112 # create the needed data structure |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
113 request = dict(product=product, component=component, |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
114 summary=title, version=version, |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
115 op_sys='All', platform='All',) |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
116 |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
117 # add CC, if given |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
118 if cc: |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
119 if isinstance(cc, basestring): |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
120 cc=[cc] |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
121 users = [] |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
122 for match in cc: |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
123 user = self.users(match)['users'] |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
124 assert len(user) == 1, 'Non-unique user: %s' % match |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
125 users.append(user[0]) |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
126 request['cc'] = users |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
127 |
31 | 128 # add blocks, if given: |
129 if blocks: | |
130 blocks = [int(i) for i in blocks] | |
131 request['blocks'] = blocks | |
132 | |
32 | 133 # add depends_on, if given |
134 if depends_on: | |
135 depends_on = [int(i) for i in depends_on] | |
136 request['depends_on'] = depends_on | |
137 | |
20 | 138 # get the bug description |
139 if not description: | |
140 description = tmpbuffer() | |
141 assert description, "Must provide a non-empty description" | |
26
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
142 request['comments'] = [self._comment(description)] |
27 | 143 |
23
2eaf17cb07f6
add whiteboard to new entry
Jeff Hammel <jhammel@mozilla.com>
parents:
21
diff
changeset
|
144 # add whiteboard, if given |
2eaf17cb07f6
add whiteboard to new entry
Jeff Hammel <jhammel@mozilla.com>
parents:
21
diff
changeset
|
145 if whiteboard: |
2eaf17cb07f6
add whiteboard to new entry
Jeff Hammel <jhammel@mozilla.com>
parents:
21
diff
changeset
|
146 if isinstance(whiteboard, basestring): |
2eaf17cb07f6
add whiteboard to new entry
Jeff Hammel <jhammel@mozilla.com>
parents:
21
diff
changeset
|
147 whiteboard=[whiteboard] |
2eaf17cb07f6
add whiteboard to new entry
Jeff Hammel <jhammel@mozilla.com>
parents:
21
diff
changeset
|
148 whiteboard = ''.join(['[%s]' % i for i in whiteboard]) |
2eaf17cb07f6
add whiteboard to new entry
Jeff Hammel <jhammel@mozilla.com>
parents:
21
diff
changeset
|
149 request['whiteboard'] = whiteboard |
2eaf17cb07f6
add whiteboard to new entry
Jeff Hammel <jhammel@mozilla.com>
parents:
21
diff
changeset
|
150 |
20 | 151 # POST the request |
152 try: | |
153 results = self._request('/bug', request) | |
154 except Exception, e: | |
155 raise | |
156 | |
157 # return the URL | |
158 return results['ref'] | |
159 | |
35
94b0b7b4f190
attachment filename for bzapi
Jeff Hammel <jhammel@mozilla.com>
parents:
34
diff
changeset
|
160 def attach(self, bug, attachment, description=None, reviewer=None, comment=None): |
34
1ce13b2b54a4
stubbing adding attachment; incomplete
Jeff Hammel <jhammel@mozilla.com>
parents:
32
diff
changeset
|
161 """ |
1ce13b2b54a4
stubbing adding attachment; incomplete
Jeff Hammel <jhammel@mozilla.com>
parents:
32
diff
changeset
|
162 add an attachment to a bug |
1ce13b2b54a4
stubbing adding attachment; incomplete
Jeff Hammel <jhammel@mozilla.com>
parents:
32
diff
changeset
|
163 |
1ce13b2b54a4
stubbing adding attachment; incomplete
Jeff Hammel <jhammel@mozilla.com>
parents:
32
diff
changeset
|
164 - bug: bug number to attach to |
35
94b0b7b4f190
attachment filename for bzapi
Jeff Hammel <jhammel@mozilla.com>
parents:
34
diff
changeset
|
165 - attachment: file or URL of attachment |
34
1ce13b2b54a4
stubbing adding attachment; incomplete
Jeff Hammel <jhammel@mozilla.com>
parents:
32
diff
changeset
|
166 - reviewer: flag for review (r?) |
1ce13b2b54a4
stubbing adding attachment; incomplete
Jeff Hammel <jhammel@mozilla.com>
parents:
32
diff
changeset
|
167 - comment: add this comment to the bug |
1ce13b2b54a4
stubbing adding attachment; incomplete
Jeff Hammel <jhammel@mozilla.com>
parents:
32
diff
changeset
|
168 """ |
1ce13b2b54a4
stubbing adding attachment; incomplete
Jeff Hammel <jhammel@mozilla.com>
parents:
32
diff
changeset
|
169 |
1ce13b2b54a4
stubbing adding attachment; incomplete
Jeff Hammel <jhammel@mozilla.com>
parents:
32
diff
changeset
|
170 if not description: |
36
619e27c447b8
correct variable name + comment
Jeff Hammel <jhammel@mozilla.com>
parents:
35
diff
changeset
|
171 # fairly hacky :/ |
619e27c447b8
correct variable name + comment
Jeff Hammel <jhammel@mozilla.com>
parents:
35
diff
changeset
|
172 description = attachment |
34
1ce13b2b54a4
stubbing adding attachment; incomplete
Jeff Hammel <jhammel@mozilla.com>
parents:
32
diff
changeset
|
173 |
35
94b0b7b4f190
attachment filename for bzapi
Jeff Hammel <jhammel@mozilla.com>
parents:
34
diff
changeset
|
174 # read contents |
94b0b7b4f190
attachment filename for bzapi
Jeff Hammel <jhammel@mozilla.com>
parents:
34
diff
changeset
|
175 if '://' in attachment: |
94b0b7b4f190
attachment filename for bzapi
Jeff Hammel <jhammel@mozilla.com>
parents:
34
diff
changeset
|
176 # URL |
94b0b7b4f190
attachment filename for bzapi
Jeff Hammel <jhammel@mozilla.com>
parents:
34
diff
changeset
|
177 basename = attachment.rstrip('/').rsplit('/', 1)[-1] |
94b0b7b4f190
attachment filename for bzapi
Jeff Hammel <jhammel@mozilla.com>
parents:
34
diff
changeset
|
178 contents = urllib2.urlopen(attachment).read() |
94b0b7b4f190
attachment filename for bzapi
Jeff Hammel <jhammel@mozilla.com>
parents:
34
diff
changeset
|
179 else: |
94b0b7b4f190
attachment filename for bzapi
Jeff Hammel <jhammel@mozilla.com>
parents:
34
diff
changeset
|
180 # file path |
94b0b7b4f190
attachment filename for bzapi
Jeff Hammel <jhammel@mozilla.com>
parents:
34
diff
changeset
|
181 basename = os.path.basename(attachment) |
94b0b7b4f190
attachment filename for bzapi
Jeff Hammel <jhammel@mozilla.com>
parents:
34
diff
changeset
|
182 contents = file(attachment).read() |
94b0b7b4f190
attachment filename for bzapi
Jeff Hammel <jhammel@mozilla.com>
parents:
34
diff
changeset
|
183 |
38
0a3c7d8eec72
utilize patch to determine if a patch or not
Jeff Hammel <jhammel@mozilla.com>
parents:
36
diff
changeset
|
184 is_patch = bool(patch.fromstring(contents)) |
40 | 185 if is_patch: |
186 content_type = 'text/plain' | |
187 else: | |
188 # TODO: better content_type | |
189 content_type = 'text/plain' | |
38
0a3c7d8eec72
utilize patch to determine if a patch or not
Jeff Hammel <jhammel@mozilla.com>
parents:
36
diff
changeset
|
190 |
0a3c7d8eec72
utilize patch to determine if a patch or not
Jeff Hammel <jhammel@mozilla.com>
parents:
36
diff
changeset
|
191 # patch flags |
0a3c7d8eec72
utilize patch to determine if a patch or not
Jeff Hammel <jhammel@mozilla.com>
parents:
36
diff
changeset
|
192 flags = [] |
0a3c7d8eec72
utilize patch to determine if a patch or not
Jeff Hammel <jhammel@mozilla.com>
parents:
36
diff
changeset
|
193 if reviewer: |
0a3c7d8eec72
utilize patch to determine if a patch or not
Jeff Hammel <jhammel@mozilla.com>
parents:
36
diff
changeset
|
194 # from |
0a3c7d8eec72
utilize patch to determine if a patch or not
Jeff Hammel <jhammel@mozilla.com>
parents:
36
diff
changeset
|
195 # https://github.com/toolness/pybugzilla/blob/master/bzpatch.py#L177 |
39 | 196 flags.append({'name': 'review', |
197 'requestee': {'name': reviewer}, | |
198 'status': '?', | |
199 'type_id': 4 # yay for magic numbers! :/ | |
200 }) | |
38
0a3c7d8eec72
utilize patch to determine if a patch or not
Jeff Hammel <jhammel@mozilla.com>
parents:
36
diff
changeset
|
201 |
34
1ce13b2b54a4
stubbing adding attachment; incomplete
Jeff Hammel <jhammel@mozilla.com>
parents:
32
diff
changeset
|
202 # create attachment data structure |
1ce13b2b54a4
stubbing adding attachment; incomplete
Jeff Hammel <jhammel@mozilla.com>
parents:
32
diff
changeset
|
203 # https://wiki.mozilla.org/Bugzilla:REST_API:Objects#Attachment |
40 | 204 contents = contents.encode('base64') |
41 | 205 attachment= {'content_type': content_type, |
40 | 206 'data': contents, |
34
1ce13b2b54a4
stubbing adding attachment; incomplete
Jeff Hammel <jhammel@mozilla.com>
parents:
32
diff
changeset
|
207 'description': description, |
40 | 208 'encoding': 'base64', |
38
0a3c7d8eec72
utilize patch to determine if a patch or not
Jeff Hammel <jhammel@mozilla.com>
parents:
36
diff
changeset
|
209 'file_name': basename, |
0a3c7d8eec72
utilize patch to determine if a patch or not
Jeff Hammel <jhammel@mozilla.com>
parents:
36
diff
changeset
|
210 'flags': flags, |
0a3c7d8eec72
utilize patch to determine if a patch or not
Jeff Hammel <jhammel@mozilla.com>
parents:
36
diff
changeset
|
211 'is_patch': is_patch, |
40 | 212 'is_private': False, |
213 'size': len(contents) | |
34
1ce13b2b54a4
stubbing adding attachment; incomplete
Jeff Hammel <jhammel@mozilla.com>
parents:
32
diff
changeset
|
214 } |
1ce13b2b54a4
stubbing adding attachment; incomplete
Jeff Hammel <jhammel@mozilla.com>
parents:
32
diff
changeset
|
215 |
40 | 216 return self._request('/bug/%s/attachment' % bug, attachment) |
34
1ce13b2b54a4
stubbing adding attachment; incomplete
Jeff Hammel <jhammel@mozilla.com>
parents:
32
diff
changeset
|
217 |
20 | 218 def configuration(self): |
219 """bugzilla configuration""" | |
27 | 220 |
20 | 221 if not hasattr(self, '_configuration'): |
222 config_cache = os.path.join(os.environ['HOME'], self.config_cache) | |
223 if not self.refresh: | |
224 try: | |
225 self._configuration = json.loads(file(config_cache).read()) | |
226 except: | |
227 pass | |
228 if not getattr(self, '_configuration', None): | |
229 self._configuration = self._request('/configuration') | |
230 if not self.print_request: | |
231 f = file(config_cache, 'w') | |
232 print >> f, json.dumps(self._configuration) | |
233 self.refresh = False | |
234 return self._configuration | |
235 | |
236 ### internal methods | |
237 | |
238 def _comment(self, text): | |
239 retval = {'is_private': False, 'text': text} | |
240 return retval | |
241 | |
242 def _request(self, path, data=None): | |
243 url = self.server + path | |
244 query = {} | |
245 if self.username: | |
246 query['username'] = self.username | |
247 if self.password: | |
248 query['password'] = self.password | |
249 if query: | |
250 query = urllib.urlencode(query) | |
26
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
251 joiner = '?' in url and '&' or '?' |
85357b075211
add ability to CC and use multiple whiteboard
Jeff Hammel <jhammel@mozilla.com>
parents:
25
diff
changeset
|
252 url += joiner + query |
20 | 253 headers = {'Accept': 'application/json', |
254 'Content-Type': 'application/json'} | |
255 if data: | |
256 data = json.dumps(data) | |
257 req = urllib2.Request(url, data, headers) | |
258 | |
259 # print out the request | |
260 # from http://stackoverflow.com/questions/603856/how-do-you-get-default-headers-in-a-urllib2-request | |
261 if self.print_request: | |
262 | |
263 f = file(self.print_request, 'a') | |
264 class MyHTTPConnection(httplib.HTTPConnection): | |
265 def send(self, s): | |
266 print >> f, s # or save them, or whatever! | |
267 httplib.HTTPConnection.send(self, s) | |
268 | |
269 class MyHTTPSConnection(httplib.HTTPSConnection): | |
270 def send(self, s): | |
271 print >> f, s | |
272 httplib.HTTPSConnection.send(self, s) | |
273 | |
274 class MyHTTPHandler(urllib2.HTTPHandler): | |
275 def http_open(self, req): | |
276 return self.do_open(MyHTTPConnection, req) | |
277 class MyHTTPSHandler(urllib2.HTTPSHandler): | |
278 def https_open(self, req): | |
279 return self.do_open(MyHTTPSConnection, req) | |
280 | |
281 if self.server.startswith('https://'): | |
282 opener = urllib2.build_opener(MyHTTPSHandler) | |
283 else: | |
284 opener = urllib2.build_opener(MyHTTPHandler) | |
285 opener.open(req) | |
286 return | |
30 | 287 |
20 | 288 try: |
289 response = urllib2.urlopen(req) | |
290 except Exception, e: | |
30 | 291 print e |
292 if data: | |
293 print data | |
20 | 294 import pdb; pdb.set_trace() |
295 the_page = response.read() | |
296 return json.loads(the_page) |