Source code for IGitt.GitHub

"""
This package contains the GitHub implementations of the interfaces in
server.git.Interfaces.
"""
from datetime import datetime
from typing import Optional
import os
import logging

from requests_oauthlib import OAuth2
import jwt

from IGitt.Interfaces import Token, get, post
from IGitt.Utils import CachedDataMixin


GH_INSTANCE_URL = os.environ.get('GH_INSTANCE_URL', 'https://github.com')
if not GH_INSTANCE_URL.startswith('http'):  # dont cover cause it'll be removed
    GH_INSTANCE_URL = 'https://' + GH_INSTANCE_URL
    logging.warning('Include the protocol in GH_INSTANCE_URL! Omitting it has '
                    'been deprecated.')
BASE_URL = GH_INSTANCE_URL.replace('github.com', 'api.github.com')


[docs]class GitHubMixin(CachedDataMixin): """ Base object for things that are on GitHub. """ def _get_data(self): return get(self._token, self.url)
[docs] @staticmethod def absolute_url(url): """ Makes a URL like ``/repo/coala/coala`` absolute. """ return BASE_URL + url
@property def hoster(self): """ Returns `github`. """ return 'github' @property def url(self): """ Returns github API url. """ return self.absolute_url(self._url) @property def web_url(self): """ Returns the web link for GitHub. """ return self.data['html_url'] def __repr__(self): # dont cover return '<{} object(url={}) at {}>'.format(self.__class__.__name__, self.url, hex(id(self)))
[docs]class GitHubToken(Token): """ Object representation of oauth tokens. """ def __init__(self, token): self._token = token @property def headers(self): """ GitHub Access token does not require any special headers. """ return {} @property def parameter(self): """ No additional query parameters are used with GitHub token. """ return {} @property def value(self): return self._token @property def auth(self): return OAuth2(token={'access_token': self._token, 'token_type': 'bearer'})
[docs]class GitHubJsonWebToken(Token): """ Object representation of JSON Web Token. """ def __init__(self, private_key: str, app_id: int): self._key = private_key.strip() self._app_id = app_id self._payload = None self._jwt_token = None @property def payload(self): """ Returns the payload to be sent for JWT encoding. """ if not self._payload: self._payload = { # issued at time 'iat': int(datetime.now().timestamp()), # JWT expiration time (10 minute maximum), minus 5 seconds just # to be sure and cover up the request time 'exp': int(datetime.now().timestamp() + (10 * 60) - 5), # GitHub App's identifier 'iss': self._app_id } return self._payload # testing over recorded requests is unadvisable as it is dependent on the # time of execution of tests @property def is_expired(self): # dont cover """ Returns True if the JWT has expired. """ return self.payload['exp'] < datetime.now().timestamp() @property def headers(self): return {'Authorization': 'Bearer {}'.format(self.value), 'Accept': 'application/vnd.github.machine-man-preview+json'} @property def parameter(self): """ GitHub's JSON Web Token can only be authenticated via the ``Authorization`` header and so, all the nested requests have to be made in only that way. """ return {} @property def value(self): if not self._jwt_token or self.is_expired: self._jwt_token = jwt.encode(self.payload, self._key, 'RS256') return self._jwt_token.decode('utf-8') @property def auth(self): """ OAuth 2.0 JWT Bearer Token Flow is not supported by oauthlib yet. Reference: https://github.com/oauthlib/oauthlib/issues/50 """ return None
[docs]class GitHubInstallationToken(Token): """ Object representation of GitHub Installation Token. """ def __init__(self, installation_id: int, jwt_token: GitHubJsonWebToken, token: Optional[str]=None, expiry: Optional[datetime]=None): self._jwt = jwt_token self._expiry = expiry self._token = token self._id = installation_id @property def jwt(self): """ Retrieves the JWT being used. """ return self._jwt @property def headers(self): return {'Authorization': 'token {}'.format(self.value), 'Accept': 'application/vnd.github.machine-man-preview+json'} # testing over recorded requests is unadvisable as it is dependent on the # time of execution of tests @property def is_expired(self): # dont cover """ Returns true if the token has expired. """ if not self._expiry: return True return datetime.utcnow() > self._expiry def _get_new_token(self): data = post(self._jwt, BASE_URL+'/installations/{}/access_tokens'.format(self._id), {}) return data['token'], datetime.strptime(data['expires_at'], '%Y-%m-%dT%H:%M:%SZ') @property def value(self): if self.is_expired or not self._token: self._token, self._expiry = self._get_new_token() return self._token @property def parameter(self): """ GitHub Installation Token can only be authenticated via the ``Authorization`` header and so, all the nested requests have to be made in only that way. """ return {} @property def auth(self): """ OAuth 2.0 JWT Bearer Token Flow is not supported by oauthlib yet. Reference: https://github.com/oauthlib/oauthlib/issues/50 """ return None