-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathRedmineAPI.py
More file actions
234 lines (198 loc) · 10.1 KB
/
RedmineAPI.py
File metadata and controls
234 lines (198 loc) · 10.1 KB
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
import requests
import logging
from urllib.parse import urljoin
class RedmineInterface(object):
def __init__(self, url, api_key, wait_between_retry_attempts=60):
"""
:param url: Redmine url in this format - http://redmine.biodiversity.agr.gc.ca/
:param api_key: Your redmine api key - You can find your API key on your account page ( /my/account )
when logged in, on the right-hand pane of the default layout.
:param wait_between_retry_attempts: How many seconds to wait between retry attempts when accessing Redmine
"""
self.logger = logging.getLogger(__name__)
self.logger.setLevel('DEBUG')
if self.__url_validator(url):
self.url = url
else:
raise RedmineConnectionError("Invalid URL")
self.wait = wait_between_retry_attempts
self.api_key = api_key
def upload_file(self, filepath, issue_id, content_type, file_name_once_uploaded="",
additional_notes="", status_change=None):
"""
:param filepath: Path to the file you want to upload
:param issue_id: ID of the Redmine issue you want to upload the file to
:param content_type: The content type of the file you are uploading, please check on this webpage -
http://www.freeformatter.com/mime-types-list.html
:param file_name_once_uploaded: The filename once it's on Redmine
:param additional_notes: Notes to upload the file with
:param status_change: Number from 1 - 4, 2 is in progress 4 is feedback
"""
url = urljoin(self.url, 'uploads.json')
headers = {'X-Redmine-API-Key': self.api_key, 'content-type': 'application/octet-stream'}
self.logger.info("Uploading %s to redmine..." % filepath)
self.logger.info("Sending POST request to %s" % filepath)
if file_name_once_uploaded == "":
import os
file_name_once_uploaded = os.path.split(filepath)[-1]
resp = requests.post(url, headers=headers, files={file_name_once_uploaded: open(filepath, "rb")})
import json
if resp.status_code == 201:
token = json.loads(resp.content.decode("utf-8"))['upload']['token']
else:
err = "Status code %s, Message %s" % (resp.status_code, resp.content.decode("utf-8"))
self.logger.error("[Error] Problem uploading file to Redmine: " + err)
raise RedmineUploadError("Failed to upload file to Redmine. Status code %s, Message %s" %
(resp.status_code, resp.content.decode("utf-8")))
data = {
"issue": {
"uploads": [
{
"token": token,
"filename": file_name_once_uploaded,
"content_type": content_type
}
],
"notes": additional_notes
}
}
if status_change is not None:
data['issue']['status_id'] = status_change
self.__put_request_timeout(urljoin(self.url, '/issues/%s.json' % str(issue_id)), data)
def get_new_issues(self, project='cfia', num_issues=25):
"""
This will return a dictionary with the newest 25 open issues
:param num_issues: Number of issues to get from redmine
:param project: in the url of your issues page
eg. http://redmine.biodiversity.agr.gc.a/projects/cfia/issues
project is cfia^^^
:return dictionary of issues
"""
self.logger.info("Getting new issues...")
url = urljoin(self.url, 'projects/%s/issues.json?limit=%d' % (project, num_issues))
return self.__get_request_timeout(url)
def get_issue_data(self, issue_id):
"""
:param issue_id: redmine issue id
:return dictionary of the issue
"""
url = urljoin(self.url, 'issues/%s.json?include=attachments' % str(issue_id))
return self.__get_request_timeout(url)
def update_issue(self, issue_id, notes=None, status_change=None, assign_to_id=None):
"""
:param issue_id: Redmine ID of the issue you want to update
:param notes: What you want to write in the notes
:param status_change: Number from 1 - 4, 2 is in progress 4 is feedback
:param assign_to_id: ID number of the user you want to assign the issue to
"""
url = urljoin(self.url, 'issues/%s.json' % str(issue_id))
data = {
"issue": {
}
}
if status_change is not None:
data['issue']['status_id'] = status_change
if assign_to_id is not None:
data['issue']['assigned_to_id'] = str(assign_to_id)
if notes is not None:
data['issue']['notes'] = notes
self.__put_request_timeout(url, data)
def download_file(self, content_url, decode=True):
"""
:param content_url: url of the file to download
:param decode: whether or not to decode the file as utf-8 (keep this on for text files)
:return: string if decoded, else a bytes type.
"""
import time
headers = {'X-Redmine-API-Key': self.api_key}
self.logger.info("Sending GET request to %s" % content_url)
resp = requests.get(content_url, headers=headers)
tries = 0
while resp.status_code != 200 and tries < 10:
if resp.status_code == 401: # Unauthorized
self.logger.info("Invalid Redmine api key!")
print(resp.content.decode('utf-8'))
raise RedmineConnectionError("Invalid Redmine api key")
self.logger.warning("GET request returned status code %d, with message %s. Waiting %ds to retry." %
(resp.status_code, resp.content.decode('utf-8'), self.wait))
time.sleep(self.wait)
self.logger.info("Retrying...")
resp = requests.get(content_url, headers=headers)
tries += 1
if tries >= 10:
raise RedmineConnectionError("Could not connect to redmine servers. Status code %d, message:\n%s"
% (resp.status_code, resp.content.decode('utf-8')))
else:
if decode:
return resp.content.decode('utf-8')
else:
return resp.content
def assign_to_author(self, issue_id, notes=None, status_change=None):
"""
:param issue_id: Redmine ID of the issue you want to update
:param notes: What you want to write in the notes
:param status_change: Number from 1 - 4, 2 is in progress 4 is feedback
"""
self.update_issue(issue_id, notes=notes, status_change=status_change,
assign_to_id=self.get_issue_data(issue_id)['issue']['author']['id'])
@staticmethod
def __url_validator(url):
from urllib import parse
qualifying = ('scheme', 'netloc')
token = parse.urlparse(str(url))
return all([getattr(token, qualifying_attr)
for qualifying_attr in qualifying])
def __get_request_timeout(self, url):
import json
import time
headers = {'X-Redmine-API-Key': self.api_key}
self.logger.info("Sending GET request to %s" % url)
resp = requests.get(url, headers=headers)
tries = 0
while resp.status_code != 200 and tries < 10:
if resp.status_code == 401: # Unauthorized
self.logger.info("Invalid Redmine api key!")
print(resp.content.decode('utf-8'))
raise RedmineConnectionError("Invalid Redmine api key")
self.logger.warning("GET request returned status code %d, with message %s. Waiting %ds to retry." %
(resp.status_code, resp.content.decode('utf-8'), self.wait))
time.sleep(self.wait)
self.logger.info("Retrying...")
resp = requests.get(url, headers=headers)
tries += 1
if tries >= 10:
raise RedmineConnectionError("Could not connect to redmine servers. Status code %d, message:\n%s"
% (resp.status_code, resp.content.decode('utf-8')))
else:
return json.loads(resp.content.decode("utf-8"))
def __put_request_timeout(self, url, data):
import time
self.wait = 60
self.logger.info("Sending PUT request to %s" % url)
headers = {'X-Redmine-API-Key': self.api_key, 'content-type': 'application/json'}
resp = requests.put(url, headers=headers, json=data)
tries = 0
while (resp.status_code != 200 and resp.status_code != 201) and tries < 10: # OK / Created
self.logger.warning("PUT request returned status code %d, with message %s. Waiting %ds to retry." %
(resp.status_code, resp.content.decode('utf-8'), self.wait))
time.sleep(self.wait)
self.logger.warning("Retrying...")
resp = requests.put(url, headers=headers, json=data)
tries += 1
if tries >= 10:
raise RedmineConnectionError("Could not connect to redmine servers. Status code %d, message:\n%s"
% (resp.status_code, resp.content.decode('utf-8')))
else:
return resp.status_code
class RedmineConnectionError(ValueError):
"""Raised when there is a problem connecting to redmine"""
def __init__(self, message, *args):
self.message = message # without this you may get DeprecationWarning
# allow users initialize misc. arguments as any other builtin Error
super(RedmineConnectionError, self).__init__(message, *args)
class RedmineUploadError(ValueError):
"""Raised when there is a problem uploading redmine file"""
def __init__(self, message, *args):
self.message = message # without this you may get DeprecationWarning
# allow users initialize misc. arguments as any other builtin Error
super(RedmineUploadError, self).__init__(message, *args)