Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tidy Errors Integration #26

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[run]
include:
errorlogparser.py
githubapiprovider.py
payloadhandler.py
payloadreceiver.py
travisapiprovider.py
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
*.pyc

.DS_Store
.coverage
htmlcov
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ language: python
python:
- "2.7"
script: python test.py
sudo: false
sudo: false
3 changes: 3 additions & 0 deletions config.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[github]
user=UserNameHere
token=SomeTokenHere
59 changes: 59 additions & 0 deletions errorlogparser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import re

class ErrorLogParser():
def parse_log(self, log, error_re):
raise NotImplementedError

class ServoErrorLogParser(ErrorLogParser):
path_key = 'path'
position_key = 'position'
body_key = 'body'

def parse_log(self, log):
error_re = "\\x1b\[94m(.+?)\\x1b\[0m:\\x1b\[93m(.+?)\\x1b\[0m:\s\\x1b\[91m(.+?)(?:\\x1b\[0m|$)"
cont_comment_re = "\t\\x1b\[\d{2}m(.+?)\\x1b\[0m"
# error_re = "File:\s(.+?)\sLine:\s(.+?)\sComment:\s(.+)"
# cont_comment_re = "(\t.+)"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of these commented lines?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, left over from personal testing. I will remove them.

matches = []
log_list = log.splitlines()

trimmed_log_list = self._trim_log(log_list, error_re)

for log_line in trimmed_log_list:
err_match = re.match(error_re, log_line)
if err_match:
matches.append(list(err_match.groups()))
else:
cont_comment_match = re.match(cont_comment_re, log_line)
if cont_comment_match:
matches[-1][-1] += "\n{}".format(list(cont_comment_match.groups())[0])

return self._process_errors(matches)


def _trim_log(self, log_list, error_re):
"""
Cut off irrelevant details so cont_comment_re doesn't match something
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be more specific? I'm having trouble visualizing the input and output of this method right now.

that isn't an error comment
"""
trimmed_log_list = log_list
err_match = None
i = 0

while not err_match and i < len(log_list):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be better structured as a standard for log in log_list: with a if err_match: conditional inside

err_match = re.match(error_re, log_list[i])
i += 1

if err_match:
trimmed_log_list = log_list[i - 1:]

return trimmed_log_list


def _process_errors(self, matches):
return (self._convert_match_to_dict(match) for match in matches)


def _convert_match_to_dict(self, match):
return {self.path_key: match[0], self.position_key: int(match[1]), self.body_key: match[2]}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know that non-constant property names were legal in Python, but it makes sense. However, I feel like the code would be clearer with the property names inlined.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this again, I agree with you. I'll change these.


212 changes: 212 additions & 0 deletions githubapiprovider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
from StringIO import StringIO
import base64
import gzip
try:
import simplejson as json
except:
import json
import urllib2


class APIProvider:
def __init__(self, user):
self.user = user


def is_new_contributor(self, username):
raise NotImplementedError


def post_comment(self, body, issue):
raise NotImplementedError


def post_review_comment(self, pr_num, commit_id, path, pos, body):
raise NotImplementedError


def add_label(self, label, issue):
raise NotImplementedError


def remove_label(self, label, issue):
raise NotImplementedError


def get_labels(self, issue):
raise NotImplementedError


def get_diff(self, url):
raise NotImplementedError


def set_assignee(self, assignee, issue):
raise NotImplementedError


class GithubApiProvider(APIProvider):
contributors_url = "https://api.github.com/repos/%s/%s/contributors?per_page=400"
post_comment_url = "https://api.github.com/repos/%s/%s/issues/%s/comments"
review_comment_url = "https://api.github.com/repos/%s/%s/pulls/%s/comments"
collaborators_url = "https://api.github.com/repos/%s/%s/collaborators"
issue_url = "https://api.github.com/repos/%s/%s/issues/%s"
get_label_url = "https://api.github.com/repos/%s/%s/issues/%s/labels"
add_label_url = "https://api.github.com/repos/%s/%s/issues/%s/labels"
remove_label_url = "https://api.github.com/repos/%s/%s/issues/%s/labels/%s"

def __init__(self, user, token, owner, repo):
APIProvider.__init__(self, user)
self.token = token

self.owner = owner
self.repo = repo

def api_req(self, method, url, data=None, media_type=None):
data = None if not data else json.dumps(data)
headers = {} if not data else {'Content-Type': 'application/json'}
req = urllib2.Request(url, data, headers)
req.get_method = lambda: method
if self.token:
base64string = base64.standard_b64encode('%s:%s' % (self.user, self.token)).replace('\n', '')
req.add_header("Authorization", "Basic %s" % base64string)

if media_type:
req.add_header("Accept", media_type)
f = urllib2.urlopen(req)
header = f.info()
if header.get('Content-Encoding') == 'gzip':
buf = StringIO(f.read())
f = gzip.GzipFile(fileobj=buf)

return { "header": header, "body": f.read() }


# This function is adapted from https://github.com/kennethreitz/requests/blob/209a871b638f85e2c61966f82e547377ed4260d9/requests/utils.py#L562
# Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
def parse_header_links(self, value):
if not value:
return None

links = {}
replace_chars = " '\""
for val in value.split(","):
try:
url, params = val.split(";", 1)
except ValueError:
url, params = val, ''

url = url.strip("<> '\"")

for param in params.split(";"):
try:
key, value = param.split("=")
except ValueError:
break
key = key.strip(replace_chars)
if key == 'rel':
links[value.strip(replace_chars)] = url

return links


def is_new_contributor(self, username):
url = self.contributors_url % (self.owner, self.repo)
# iterate through the pages to try and find the contributor
while True:
stats_raw = self.api_req("GET", url)
stats = json.loads(stats_raw['body'])
links = self.parse_header_links(stats_raw['header'].get('Link'))

for contributor in stats:
if contributor['login'] == username:
return False

if not links or 'next' not in links:
return True

url = links['next']


def post_comment(self, body, issue):
try:
result = self.api_req("POST", self.post_comment_url % (self.owner, self.repo, issue),
{"body": body})
except urllib2.HTTPError, e:
if e.code == 201:
pass
else:
raise e


def post_review_comment(self, pr_num, commit_id, path, pos, body):
try:
result = self.api_req("POST", self.review_comment_url % (self.owner, self.repo, pr_num),
{"body": body, "commit_id":commit_id, "path":path, "position":pos})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move the { under "POST" on the previous line.

except urllib2.HTTPError, e:
if e.code == 201:
pass
else:
raise e


def get_review_comments(self, pr_num):
try:
result = self.api_req("GET", self.review_comment_url % (self.owner, self.repo, pr_num))

return json.loads(result['body'])
except urllib2.HTTPError, e:
if e.code == 201:
pass
else:
raise e


def add_label(self, label, issue):
try:
result = self.api_req("POST", self.add_label_url % (self.owner, self.repo, issue),
[label])
except urllib2.HTTPError, e:
if e.code == 201:
pass
else:
raise e


def remove_label(self, label, issue):
try:
result = self.api_req("DELETE", self.remove_label_url % (self.owner, self.repo, issue, label), {})
except urllib2.HTTPError, e:
#if e.code == 201:
# pass
#else:
# raise e
pass


def get_labels(self, issue):
try:
result = self.api_req("GET", self.get_label_url % (self.owner, self.repo, issue))
except urllib2.HTTPError, e:
if e.code == 201:
pass
else:
raise e
return map(lambda x: x["name"], json.loads(result['body']))


def get_diff(self, diff_url):
return self.api_req("GET", diff_url)['body']


def set_assignee(self, assignee, issue):
try:
result = self.api_req("PATCH", self.issue_url % (self.owner, self.repo, issue),
{"assignee": assignee})

return result['body']
except urllib2.HTTPError, e:
if e.code == 201:
pass
else:
raise e
Loading