-
Notifications
You must be signed in to change notification settings - Fork 3
/
redmineapi.py
343 lines (275 loc) · 11.5 KB
/
redmineapi.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
""" Python library for interacting with a Redmine installation.
This library will allow you to interact with Redmine issues and projects,
including listing, creating and resolving issues.
PLEASE NOTE: This project is nowhere near finished. Still needs a lot of refinement.
As of now (6/11/11):
issue retrieval and posting work
project retrieval works
TODO:
finish Issue and Project classes
finish issue posting and updating
start project posting and updating
documentation
general clean up
"""
__author__ = "Brendan Smith ([email protected])"
__version__ = "0.0.1"
__copyright__ = "Copyright (c) 2010 National Priorities Project"
__license__ = "BSD"
import datetime
import sys
import warnings
if sys.version_info[0] == 3:
from urllib.parse import urlencode
from urllib.request import urlopen
from urllib.error import HTTPError
else:
from urllib import urlencode
from urllib2 import urlopen, Request
from urllib2 import HTTPError
import httplib2 #turns out httplib2 is actually a lot better at handling JSON POST connections
try:
import json
except ImportError:
import simplejson as json
import logging
logger = logging.getLogger('redmine-api')
logger.setLevel(logging.INFO)
formatter = logging.Formatter('[%(levelname)s] %(message)s')
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
ISSUE_STATUS = {}
ISSUE_STATUS['new'] = 1
ISSUE_STATUS['resolved'] = 3
ISSUE_STATUS['closed'] = 5
class RedmineApiError(Exception):
""" Exception for Redmine API errors """
class RedmineApiObject(object):
def __init__(self, d):
self.__dict__ = d
class Issue(RedmineApiObject):
''' Redmine Issue Object '''
def __init__(self, d, redmine=None):
self.__dict__ = d
self.redmine = redmine
def __repr__(self):
return "Redmine Issue Object"
def __str__(self):
return str(self.__dict__)
#return '%s - %s - %s - %s' % (self.id,
# self.project['name'],
# self.author['name'],
# self.subject)
def __unicode__(self):
return '%s - %s - %s \n\n %s' % (self.id,
self.author['name'],
self.subject,
self.description)
def newFromApi(self, content):
d = json.loads(content)['issue']
self.__dict__.update(d)
def save(self):
''' saves a new issue to the given API instance from Issue instance
if you need to update an issue, use update() instead.
'''
new_issue = {
'issue': self.__dict__}
content = self.redmine._apiPost('issues', new_issue)
return self.newFromApi(content)
def update(self, data=None):
if data is None:
issue = {'issue': self.__dict__,}
else:
issue = {'issue': data,}
content = self.redmine._apiPut('issues/%s' % self.id, issue)
if content.strip():
return self.newFromApi(content)
def close(self, notes=None):
self.update_status(ISSUE_STATUS['closed'], notes=notes)
def resolve(self, notes=None):
self.update_status(ISSUE_STATUS['resolved'], notes=notes)
def update_status(self, status_id, notes=None):
issue_id = self.id
issue = {
"issue": {
"status_id": status_id,
# 'notes': '%s <br/><br/><br/>%s' % (notes, '*resolved from python-redmine api*')
}
}
content = self.redmine._apiPut('issues/%s' % issue_id, issue)
def annotate(self, notes):
issue_id = self.id
issue = {"issue": {'notes': notes}}
content = self.redmine._apiPut('issues/%s' % issue_id, issue)
def assign_to(self, user_id):
'''
Takes redmine user id as input and assigns the issue to that user.
Also changes the issue status to selected.
'''
data = self.__dict__
data['assigned_to'] = user_id
data['status_id'] = 12
return self.update(data)
@property
def last_updated(self):
'''
Return python datetime object for last updation date.
'''
try:
return datetime.datetime.strptime(self.updated_on[:-6], '%Y/%m/%d %H:%M:%S')
except ValueError:
pass
class Project(RedmineApiObject):
''' Redmine Project Object '''
def __str__(self):
#return '%s - %s - %s' % (self.id, self.name, self.description)
return self.__dict__
def __repr__(self):
return "New Redmine Project Object"
class User(RedmineApiObject):
''' Redmine User Object '''
def __str__(self):
return '%s - %s - %s, %s' % (self.id, self.login,
self.lastname, self.firstname)
def __repr__(self):
return "New Redmine User Object"
class Redmine(object):
def __init__(self, hostname=None, apikey=None, ssl=False, easy_ssl=True):
self.apikey = apikey
self.hostname = hostname
self.ssl = ssl
self.easy_ssl = easy_ssl
if self.ssl:
self.scheme = 'https'
else:
self.scheme = 'http'
def check_reqs(self, func=None):
if self.hostname is None:
raise RedmineApiError('You must supply a Hostname to your redmine installation.')
if self.apikey is None:
raise RedmineApiError('Missing Redmine API Key.')
if func is None:
raise RedmineApiError('Missing function call.')
def _apiGet(self, func, params=None, extra_querystring=''):
self.check_reqs(func)
api_url = '%s://%s/%s.json?key=%s&%s' % (self.scheme, self.hostname,
func, self.apikey, extra_querystring)
try:
h = httplib2.Http(disable_ssl_certificate_validation=self.easy_ssl)
if params is not None and 'issue' in params.keys():
params['issue']['redmine'] = ''
resp, content = h.request(api_url,
'GET',
json.dumps(params),
headers={'Content-Type': 'application/json'})
return content
except HTTPError, e:
raise RedmineApiError("HTTPError - %s" % e.read())
except (ValueError, KeyError), e:
raise RedmineApiError('Invalid Response - %s', e.code())
def _apiPost(self, func, params=None):
self.check_reqs(func)
api_url = '%s://%s/%s.json?key=%s' % (self.scheme, self.hostname,
func, self.apikey)
logger.info(api_url)
try:
h = httplib2.Http(disable_ssl_certificate_validation=self.easy_ssl)
if params is not None:
params['issue']['redmine'] = ''
resp, content = h.request(api_url,
'POST',
json.dumps(params),
headers={'Content-Type': 'application/json'})
return content
except HTTPError, e:
raise RedmineApiError("HTTPError - %s" % e.read())
except (ValueError, KeyError), e:
raise RedmineApiError('Invalid Response - %s' % e.code())
def _apiPut(self, func, params=None):
self.check_reqs(func)
api_url = '%s://%s/%s.json?key=%s' % (self.scheme, self.hostname, func, self.apikey)
try:
h = httplib2.Http(disable_ssl_certificate_validation=self.easy_ssl)
if params is not None:
params['issue']['redmine'] = ''
resp, content = h.request(api_url,
'PUT',
json.dumps(params),
headers={'Content-Type': 'application/json'})
#note: PUT doesn't include any content in the response
if resp['status'] != '200':
raise Exception('Invalid response %s', resp['status'])
return content
except HTTPError, e:
raise RedmineApiError("HTTPError - %s" % e.read())
except (ValueError, KeyError), e:
raise RedmineApiError('Invalid Response - %s' % e.code())
class _issues(object):
def __init__(self, redmine):
self.redmine = redmine
def get(self, issue_id):
#issue_id = kwargs=['issue_id']
if issue_id is None:
raise RedmineApiError("You must provide an issue_id")
result = json.loads(self.redmine._apiGet('issues/%s' % issue_id))
return Issue(result['issue'], redmine=self.redmine)
def set(self, **kwargs):
issue_id = kwargs['issue_id']
if issue_id is None:
raise RedmineApiError("You must provide an issue_id")
#get the issue first
data = json.loads(self.redmine._apiGet('issues/%s' % issue_id))
#update with k/v. Should probably only update the allowed ones.
for k, v in kwargs.iteritems():
data['issue'][k] = v
result = self.redmine._apiPut('issues/%s' % issue_id, data)
#PUT doesnt seem to return anything, so get the issue, and return it.
return self.get(**kwargs)
def getList(self, **kwargs):
querystring = {'status_id': '*'}
querystring.update(kwargs)
querystring = urlencode(querystring)
results = json.loads(self.redmine._apiGet('issues', kwargs, extra_querystring=querystring))
return [Issue(i, redmine=self.redmine) for i in results['issues']]
def new(self, issue):
if issue is None:
raise RedmineApiError("You must provide an Issue Object or a dictionary")
if isinstance(issue, Issue):
new_issue = {'issue': issue.__dict__}
else:
new_issue = {'issue': issue }
content = self.redmine._apiPost('issues', new_issue)
@property
def issues(self):
return self._issues(self)
class _projects(object):
def __init__(self, redmine):
self.redmine = redmine
def get(self, **kwargs):
project_id = kwargs['project_id']
if project_id is None:
raise RedmineApiError("You must provide a project_id")
result = json.loads(self.redmine._apiGet('projects/%s' % project_id))['project']
return Project(result)
def getList(self, **kwargs):
results = json.loads(self.redmine._apiGet('projects', kwargs))
return [Project(p) for p in results['projects']]
@property
def projects(self):
return self._projects(self)
class _users(object):
def __init__(self, redmine):
self.redmine = redmine
def get(self, **kwargs):
user_id = kwargs['user_id']
if user_id is None:
raise RedmineApiError("You must provide a user_id")
result = json.loads(self.redmine._apiGet('users/%s' % user_id))['user']
return User(result)
def getList(self, **kwargs):
results = json.loads(self.redmine._apiGet('users', kwargs))
return [User(u) for u in results['users']]
@property
def users(self):
return self._users(self)