| OLD | NEW |
| 1 """Provides API wrapper for the codesite issue tracker""" | 1 """Provides API wrapper for the codesite issue tracker""" |
| 2 | 2 |
| 3 import httplib2 | 3 from endpoints import endpoints |
| 4 import logging | |
| 5 import time | |
| 6 | |
| 7 from apiclient import discovery | |
| 8 from apiclient.errors import HttpError | |
| 9 from issue_tracker.issue import Issue | 4 from issue_tracker.issue import Issue |
| 10 from issue_tracker.comment import Comment | 5 from issue_tracker.comment import Comment |
| 11 from oauth2client.appengine import AppAssertionCredentials | |
| 12 | |
| 13 | |
| 14 # TODO(akuegel): Do we want to use a different timeout? Do we want to use a | |
| 15 # cache? See documentation here: | |
| 16 # https://github.com/jcgregorio/httplib2/blob/master/python2/httplib2/__init__.p
y#L1142 | |
| 17 def _createHttpObject(scope): # pragma: no cover | |
| 18 credentials = AppAssertionCredentials(scope=scope) | |
| 19 return credentials.authorize(httplib2.Http()) | |
| 20 | |
| 21 | |
| 22 def _buildClient(api_name, api_version, http, | |
| 23 discovery_url): # pragma: no cover | |
| 24 # This occassionally hits a 503 "Backend Error". Hopefully a simple retry | |
| 25 # can recover. | |
| 26 tries_left = 5 | |
| 27 tries_wait = 10 | |
| 28 while tries_left: | |
| 29 tries_left -= 1 | |
| 30 try: | |
| 31 client = discovery.build( | |
| 32 api_name, api_version, | |
| 33 discoveryServiceUrl=discovery_url, | |
| 34 http=http) | |
| 35 break | |
| 36 except HttpError as e: | |
| 37 if tries_left: | |
| 38 logging.error( | |
| 39 'apiclient.discovery.build() failed for %s: %s', api_name, e) | |
| 40 logging.error( | |
| 41 'Retrying apiclient.discovery.build() in %s seconds.', tries_wait) | |
| 42 time.sleep(tries_wait) | |
| 43 else: | |
| 44 logging.exception( | |
| 45 'apiclient.discovery.build() failed for %s too many times.', | |
| 46 api_name) | |
| 47 raise e | |
| 48 return client | |
| 49 | 6 |
| 50 | 7 |
| 51 class IssueTrackerAPI(object): # pragma: no cover | 8 class IssueTrackerAPI(object): # pragma: no cover |
| 52 CAN_ALL = 'all' | 9 CAN_ALL = 'all' |
| 53 | 10 |
| 54 """A wrapper around the issue tracker api.""" | 11 """A wrapper around the issue tracker api.""" |
| 55 def __init__(self, project_name): | 12 def __init__(self, project_name): |
| 56 self.project_name = project_name | 13 self.project_name = project_name |
| 57 | 14 self.client = endpoints.build_client( |
| 58 self.client = _buildClient( | 15 'monorail', 'v1', 'https://monorail-prod.appspot.com/_ah/api/discovery' |
| 59 'monorail', 'v1', | 16 '/v1/apis/{api}/{apiVersion}/rest') |
| 60 _createHttpObject('https://www.googleapis.com/auth/userinfo.email'), | |
| 61 'https://monorail-prod.appspot.com/_ah/api/discovery/v1/apis/{api}/' | |
| 62 '{apiVersion}/rest') | |
| 63 | |
| 64 def _retry_api_call(self, request, num_retries=5): | |
| 65 retries = 0 | |
| 66 while True: | |
| 67 try: | |
| 68 return request.execute() | |
| 69 except HttpError as e: | |
| 70 # This retries internal server (500, 503) and quota (403) errors. | |
| 71 if retries == num_retries or e.resp.status not in [403, 500, 503]: | |
| 72 raise | |
| 73 time.sleep(2**retries) | |
| 74 retries += 1 | |
| 75 | 17 |
| 76 def create(self, issue, send_email=True): | 18 def create(self, issue, send_email=True): |
| 77 body = {} | 19 body = {} |
| 78 assert issue.summary | 20 assert issue.summary |
| 79 body['summary'] = issue.summary | 21 body['summary'] = issue.summary |
| 80 if issue.description: | 22 if issue.description: |
| 81 body['description'] = issue.description | 23 body['description'] = issue.description |
| 82 if issue.status: | 24 if issue.status: |
| 83 body['status'] = issue.status | 25 body['status'] = issue.status |
| 84 if issue.owner: | 26 if issue.owner: |
| 85 body['owner'] = {'name': issue.owner} | 27 body['owner'] = {'name': issue.owner} |
| 86 if issue.labels: | 28 if issue.labels: |
| 87 body['labels'] = issue.labels | 29 body['labels'] = issue.labels |
| 88 if issue.components: | 30 if issue.components: |
| 89 body['components'] = issue.components | 31 body['components'] = issue.components |
| 90 if issue.cc: | 32 if issue.cc: |
| 91 body['cc'] = [{'name': user} for user in issue.cc] | 33 body['cc'] = [{'name': user} for user in issue.cc] |
| 92 request = self.client.issues().insert( | 34 request = self.client.issues().insert( |
| 93 projectId=self.project_name, sendEmail=send_email, body=body) | 35 projectId=self.project_name, sendEmail=send_email, body=body) |
| 94 tmp = self._retry_api_call(request) | 36 tmp = endpoints._retry__request(request) |
| 95 issue.id = int(tmp['id']) | 37 issue.id = int(tmp['id']) |
| 96 issue.dirty = False | 38 issue.dirty = False |
| 97 return issue | 39 return issue |
| 98 | 40 |
| 99 def update(self, issue, comment=None, send_email=True): | 41 def update(self, issue, comment=None, send_email=True): |
| 100 if not issue.dirty and not comment: | 42 if not issue.dirty and not comment: |
| 101 return issue | 43 return issue |
| 102 | 44 |
| 103 updates = {} | 45 updates = {} |
| 104 if 'summary' in issue.changed: | 46 if 'summary' in issue.changed: |
| (...skipping 17 matching lines...) Expand all Loading... |
| 122 | 64 |
| 123 body = {'id': issue.id, | 65 body = {'id': issue.id, |
| 124 'updates': updates} | 66 'updates': updates} |
| 125 | 67 |
| 126 if comment: | 68 if comment: |
| 127 body['content'] = comment | 69 body['content'] = comment |
| 128 | 70 |
| 129 request = self.client.issues().comments().insert( | 71 request = self.client.issues().comments().insert( |
| 130 projectId=self.project_name, issueId=issue.id, sendEmail=send_email, | 72 projectId=self.project_name, issueId=issue.id, sendEmail=send_email, |
| 131 body=body) | 73 body=body) |
| 132 self._retry_api_call(request) | 74 endpoints.retry_request(request) |
| 133 | 75 |
| 134 if issue.owner == '----': | 76 if issue.owner == '----': |
| 135 issue.owner = '' | 77 issue.owner = '' |
| 136 | 78 |
| 137 issue.dirty = False | 79 issue.dirty = False |
| 138 return issue | 80 return issue |
| 139 | 81 |
| 140 def addComment(self, issue_id, comment, send_email=True): | 82 def addComment(self, issue_id, comment, send_email=True): |
| 141 issue = self.getIssue(issue_id) | 83 issue = self.getIssue(issue_id) |
| 142 self.update(issue, comment, send_email) | 84 self.update(issue, comment, send_email) |
| 143 | 85 |
| 144 def getCommentCount(self, issue_id): | 86 def getCommentCount(self, issue_id): |
| 145 request = self.client.issues().comments().list( | 87 request = self.client.issues().comments().list( |
| 146 projectId=self.project_name, issueId=issue_id, startIndex=1, | 88 projectId=self.project_name, issueId=issue_id, startIndex=1, |
| 147 maxResults=0) | 89 maxResults=0) |
| 148 feed = self._retry_api_call(request) | 90 feed = endpoints.retry_request(request) |
| 149 return feed.get('totalResults', '0') | 91 return feed.get('totalResults', '0') |
| 150 | 92 |
| 151 def getComments(self, issue_id): | 93 def getComments(self, issue_id): |
| 152 rtn = [] | 94 rtn = [] |
| 153 | 95 |
| 154 request = self.client.issues().comments().list( | 96 request = self.client.issues().comments().list( |
| 155 projectId=self.project_name, issueId=issue_id) | 97 projectId=self.project_name, issueId=issue_id) |
| 156 feed = self._retry_api_call(request) | 98 feed = endpoints.retry_request(request) |
| 157 rtn.extend([Comment(entry) for entry in feed['items']]) | 99 rtn.extend([Comment(entry) for entry in feed['items']]) |
| 158 total_results = feed['totalResults'] | 100 total_results = feed['totalResults'] |
| 159 if not total_results: | 101 if not total_results: |
| 160 return rtn | 102 return rtn |
| 161 | 103 |
| 162 while len(rtn) < total_results: | 104 while len(rtn) < total_results: |
| 163 request = self.client.issues().comments().list( | 105 request = self.client.issues().comments().list( |
| 164 projectId=self.project_name, issueId=issue_id, startIndex=len(rtn)) | 106 projectId=self.project_name, issueId=issue_id, startIndex=len(rtn)) |
| 165 feed = self._retry_api_call(request) | 107 feed = endpoints.retry_request(request) |
| 166 rtn.extend([Comment(entry) for entry in feed['items']]) | 108 rtn.extend([Comment(entry) for entry in feed['items']]) |
| 167 | 109 |
| 168 return rtn | 110 return rtn |
| 169 | 111 |
| 170 def getFirstComment(self, issue_id): | 112 def getFirstComment(self, issue_id): |
| 171 request = self.client.issues().comments().list( | 113 request = self.client.issues().comments().list( |
| 172 projectId=self.project_name, issueId=issue_id, startIndex=0, | 114 projectId=self.project_name, issueId=issue_id, startIndex=0, |
| 173 maxResults=1) | 115 maxResults=1) |
| 174 feed = self._retry_api_call(request) | 116 feed = endpoints.retry_request(request) |
| 175 if 'items' in feed and len(feed['items']) > 0: | 117 if 'items' in feed and len(feed['items']) > 0: |
| 176 return Comment(feed['items'][0]) | 118 return Comment(feed['items'][0]) |
| 177 return None | 119 return None |
| 178 | 120 |
| 179 def getLastComment(self, issue_id): | 121 def getLastComment(self, issue_id): |
| 180 total_results = self.getCommentCount(issue_id) | 122 total_results = self.getCommentCount(issue_id) |
| 181 request = self.client.issues().comments().list( | 123 request = self.client.issues().comments().list( |
| 182 projectId=self.project_name, issueId=issue_id, | 124 projectId=self.project_name, issueId=issue_id, |
| 183 startIndex=total_results-1, maxResults=1) | 125 startIndex=total_results-1, maxResults=1) |
| 184 feed = self._retry_api_call(request) | 126 feed = endpoints.retry_request(request) |
| 185 if 'items' in feed and len(feed['items']) > 0: | 127 if 'items' in feed and len(feed['items']) > 0: |
| 186 return Comment(feed['items'][0]) | 128 return Comment(feed['items'][0]) |
| 187 return None | 129 return None |
| 188 | 130 |
| 189 def getIssue(self, issue_id): | 131 def getIssue(self, issue_id): |
| 190 """Retrieve a set of issues in a project.""" | 132 """Retrieve a set of issues in a project.""" |
| 191 request = self.client.issues().get( | 133 request = self.client.issues().get( |
| 192 projectId=self.project_name, issueId=issue_id) | 134 projectId=self.project_name, issueId=issue_id) |
| 193 entry = self._retry_api_call(request) | 135 entry = endpoints.retry_request(request) |
| 194 return Issue(entry) | 136 return Issue(entry) |
| OLD | NEW |