import requests
import json
import configparser
import re
import logging
logging.basicConfig(filename='github_bot.log', level=logging.INFO, format='%(asctime)s %(levelname)s: %(message)s')
[docs]class GitHubBot:
"""
The class realizes GitHub bot functionality such as labeling all issues at once or processing them
by one.
Attributes:
url (str): Url of issues in the specified repo (ex.: `https://api.github.com/repos/<username>/<repo>/issues`)).
default_label (str): If no rule may be applied to issue, an issue will be labeled by this string
If you have token in *auth.cfg.sample file* and labeling rules in *label.cfg* file,
you may call this class the following way
.. testsetup::
from githubissuesbot import github_bot
import os
auth_cfg_file = os.path.abspath(os.path.join(os.path.dirname(github_bot.__file__))) + '/config/auth.cfg.sample'
label_cfg_file = os.path.abspath(os.path.join(os.path.dirname(github_bot.__file__))) + '/config/label.cfg'
.. testcode::
from githubissuesbot import github_bot
bot = github_bot.GitHubBot(auth_cfg_file,
label_cfg_file,
"https://api.github.com/repos/my-username/my-repo/issues",
"default")
And access its url and default label by invoking
>>> bot.url
'https://api.github.com/repos/my-username/my-repo/issues'
>>> bot.default_label
'default'
You can also send GitHub personal access token as parameter and in this case doesn't matter what
you have in *auth_cfg_file*
.. testcode::
from githubissuesbot import github_bot
bot = github_bot.GitHubBot(auth_cfg_file,
label_cfg_file,
"https://api.github.com/repos/my-username/my-repo/issues",
"default",
auth_token='mypersonalaccesstoken')
print(bot._token)
.. testoutput::
mypersonalaccesstoken
"""
def __init__(self, auth_file, label_file, url, default_label, session=None, auth_token=None):
"""
A constructor.
Args:
auth_file (str): path to file with authorization info. If you don't want to read file,
you may use *auth_token*.
label_file (str): path to file with issue labels and their rules.
url (str): Url of issues in repo (ex.: `https://api.github.com/repos/<username>/<repo>/issues`).
If you want to label ALL issues in this repo, you MUST specify this parameter. Otherwise,
set it to None in case of labeling only one issue.
default_label (str): If no rule may be applied to issue, an issue will be labeled by this string.
session (betamax_session or requests.Session()): Session for handling network communication.
If it is None, requests.Session() will be invoked.
auth_token (str): GitHub Personal Access Token. If it is None, the *auth_file* will be read. Otherwise, the
value of this variable will be used for authorization and *auth_file* parameter will be ignored.
"""
self._read_config(auth_file, auth_token, label_file)
self.url = url
self.default_label = default_label
self._session = session or requests.Session()
self._session.headers = {'Authorization': 'token ' + self._token, 'User-Agent': 'Python'}
def _read_config(self, auth_file, auth_token, label_file):
"""
The function reads GitHub Personal Access token (from *auth_file* or use *auth_token* if it is given)
and labeling rules.
Args:
auth_file (str): File with GitHub Personal Access token.
auth_token (str): GitHub Personal Access token. If None, *auth_file* will be read.
Otherwise, the program will use the *auth_token*.
label_file (str): File with available labels (applied on issues) and their rules.
"""
conf = configparser.ConfigParser()
if auth_token:
conf.read(label_file)
self._token = auth_token
else:
conf.read([auth_file, label_file])
self._token = conf['github']['token']
self._label_list = list(map(str.strip, conf['list']['labels'].split(',')))
logging.debug("List of defined labels:", self._label_list)
self._label_rules = []
for label in self._label_list:
self._label_rules.append(conf['rules'][label])
[docs] def label_all_issues(self, label_comments):
"""
Call this function to label all unlabeled issues at once.
Args:
label_comments (bool): Set it to True, if the program must use comments
(in addition to issues themselves) for labeling.
Returns:
dict: Dictionary with numbers of issues as keys and lists of assigned labels together
with POST HTTP status code as values (ex.: results[15][0] gives labels of the 15-th issue,
and results[15][1] gives POST status code of the same issue
(if labeling on GitHub was successful or not)).
"""
r = self._session.get(self.url)
r.raise_for_status()
results = {}
for issue_info in r.json():
if issue_info['labels']:
continue
results[issue_info['number']] = self._set_labels(self.url + '/' + str(issue_info['number']),
issue_info['title'],
issue_info['body'],
label_comments)
return results
[docs] def label_issue(self, issue_info):
"""
Call this function to label only the given unlabeled issue.
Args:
issue_info (json): Issue information in JSON format.
Example issue JSON: https://developer.github.com/v3/activity/events/types/#issuesevent
Returns:
list: List of assigned labels together with POST HTTP status code
(if labeling on GitHub was successful or not). Example:
results[0] gives labels of the issue, and results[1] gives POST status code of the same issue).
"""
if not issue_info['labels']:
return self._set_labels(issue_info['url'],
issue_info['title'],
issue_info['body'], False)
def _set_labels(self, issue_url, title, body, label_comments):
"""
The function labels an issue on the given url by its title, body and comments.
Args:
issue_url (str): Url of the issue.
Example: `https://api.github.com/repos/<username>/<repo>/issues/<issue-number>
title (str): Issue's title.
body (str): Issue's body.
label_comments (bool): Set it to True, if the program must use comments
(in addition to issue's title and body) for labeling.
Returns:
list: The first value contains assigned labels, the second one - HTTP POST status code
(if labeling on GitHub was successful or not).
"""
text = title + " " + body
if label_comments:
# add comments' body to issue's text
r = self._session.get(issue_url + '/comments')
for comm in r.json():
text += ' ' + comm['body']
labels = []
for rule, label_text in zip(self._label_rules, self._label_list):
if re.search(rule, text):
labels.append(label_text)
logging.debug(10 * '-')
logging.debug('Issue url:', issue_url)
logging.debug('Issue title:', title)
if not labels:
labels.append(self.default_label)
logging.debug("Labeled as:", labels)
r = self._session.post(issue_url + '/labels', data=json.dumps(labels))
logging.debug('Status code:', r.status_code)
return [labels, r.status_code]