"""GitHub module
This module contains classes and methods for working and communicating with
GitHub API (including error handling).
"""
import hashlib
import hmac
import requests
[docs]class GitHubError(Exception):
"""GitHub error class.
This class contains methods describing and used for working with errors
returned by GitHub. Initializing an instance will parse status code
and message from GitHub response.
Example:
.. testcode::
import requests
import labelord
res = requests.Response()
res.status_code = 400
res.json = lambda: {}
gh_error = labelord.github.GitHubError(res)
.. doctest::
>>> gh_error.status_code
400
>>> gh_error.message
'No message provided'
Args:
response (requests.Response): GitHub response.
"""
def __init__(self, response):
self.status_code = response.status_code
self.message = response.json().get('message', 'No message provided')
def __str__(self):
return 'GitHub: ERROR {}'.format(self.code_message)
@property
def code_message(self, sep=' - '):
"""Formats an error.
This method returns formatted GitHub error.
Example:
.. testcode::
import requests
import labelord
res = requests.Response()
res.status_code = 400
res.json = lambda: {}
gh_error = labelord.github.GitHubError(res)
.. doctest::
>>> gh_error.code_message
'400 - No message provided'
Args:
sep (Optional[str]): Separator, default: ' - '.
Returns:
str: GitHub error in the format of code **sep** message.
"""
return sep.join([str(self.status_code), self.message])
[docs]class GitHub:
"""GitHub API class.
This class contains methods for communicating with GitHub API. Initializing
an instance will set its token and session.
Example:
.. testcode::
import requests
import labelord
github = labelord.github.GitHub('123456')
.. doctest::
>>> github.token
'123456'
.. testcode::
req = requests.Request()
github.session.auth(req)
.. doctest::
>>> req.headers['Authorization']
'token 123456'
>>> req.headers['User-Agent']
'Python/Labelord'
Args:
token (str): GitHub API token.
session (requests.Session): Session object.
"""
GH_API_ENDPOINT = 'https://api.github.com'
"""str: URL of GitHub API endpoint."""
def __init__(self, token, session=None):
self.token = token
self.set_session(session)
[docs] def set_session(self, session):
"""Sets session
This method sets new session including headers for use within class.
Args:
session (requests.Session): Session to use.
"""
self.session = session or requests.Session()
self.session.auth = self._session_auth()
def _session_auth(self):
def github_auth(req):
req.headers = {
'Authorization': 'token ' + self.token,
'User-Agent': 'Python/Labelord'
}
return req
return github_auth
def _get_raising(self, url, expected_code=200):
response = self.session.get(url)
if response.status_code != expected_code:
raise GitHubError(response)
return response
def _get_all_data(self, resource):
"""Get all data spread across multiple pages"""
response = self._get_raising('{}{}?per_page=100&page=1'.format(
self.GH_API_ENDPOINT, resource
))
yield from response.json()
while 'next' in response.links:
response = self._get_raising(response.links['next']['url'])
yield from response.json()
[docs] def list_repositories(self):
"""Gets a list of names of accessible repositories (including owner)
This method gets a list of names of GitHub repositiories accessible
with given GitHub API token.
Returns:
list: List of repository names (full, eg. User/Repo).
"""
data = self._get_all_data('/user/repos')
return [repo['full_name'] for repo in data]
[docs] def list_labels(self, repository):
"""Gets dict of labels with colors for given repository slug
This method gets labels with colors for given GitHub repository.
Args:
repository (str): GitHub repository slug (eg. User/Repo).
Returns:
dict: Color for each label in repository.
"""
data = self._get_all_data('/repos/{}/labels'.format(repository))
return {l['name']: str(l['color']) for l in data}
[docs] def create_label(self, repository, name, color, **kwargs):
"""Creates new label in given repository
This method creates new label with specified name and color in given
repository.
Args:
repository (str): GitHub repository slug (eg. User/Repo).
name (str): New label's name.
color (str): New label's color.
kwargs (dict): Other arguments placeholder, not used.
Raises:
GitHubError: If status code returned by GitHub is not 201.
"""
data = {'name': name, 'color': color}
response = self.session.post(
'{}/repos/{}/labels'.format(self.GH_API_ENDPOINT, repository),
json=data
)
if response.status_code != 201:
raise GitHubError(response)
[docs] def update_label(self, repository, name, color, old_name=None, **kwargs):
"""Updates existing label in given repository
This method updates given repository's existing label's name and color.
Args:
repository (str): GitHub repository slug (eg. User/Repo).
name (str): Updated label's new name.
color (str): Updated label's new color.
old_name (Optional[str]): Updated label's old name, default: None.
kwargs (dict): Other arguments placeholder, not used.
Raises:
GitHubError: If status code returned by GitHub is not 200.
"""
data = {'name': name, 'color': color}
response = self.session.patch(
'{}/repos/{}/labels/{}'.format(
self.GH_API_ENDPOINT, repository, old_name or name
),
json=data
)
if response.status_code != 200:
raise GitHubError(response)
[docs] def delete_label(self, repository, name, **kwargs):
"""Deletes existing label in given repository
This method deletes existing label from given repository.
Args:
repository (str): GitHub repository slug (eg. User/Repo).
name (str): Deleted label's name.
kwargs (dict): Other arguments placeholder, not used.
Raises:
GitHubError: If status code returned by GitHub is not 204.
"""
response = self.session.delete(
'{}/repos/{}/labels/{}'.format(
self.GH_API_ENDPOINT, repository, name
)
)
if response.status_code != 204:
raise GitHubError(response)
[docs] @staticmethod
def webhook_verify_signature(data, signature, secret, encoding='utf-8'):
"""Verifies webhook signature
This method verifies webhook signature.
Example:
.. testcode::
import hashlib
import hmac
import labelord
github = labelord.github.GitHub('123456')
data = 'test data'.encode('utf-8')
secret = 'secret'
signature = 'sha1=a81d790e8312e98f84aacaf95ca9dd04e5305fbf'
.. doctest::
>>> github.webhook_verify_signature(data, signature, secret)
True
.. testcode::
data = 'other data'.encode('utf-8')
.. doctest::
>>> github.webhook_verify_signature(data, signature, secret)
False
Args:
data (str): Incoming request data.
signature (str): GitHub signature.
secret (str): Webhook secret.
encoding (Optional[str]): Character encoding used for secret, default: utf-8.
Returns:
bool: Webhook signature verification result.
"""
h = hmac.new(secret.encode(encoding), data, hashlib.sha1)
return hmac.compare_digest('sha1=' + h.hexdigest(), signature)