bioarchlinux-tools/agithub.py
2022-05-12 01:48:42 +00:00

202 lines
6.1 KiB
Python

from __future__ import annotations
import datetime
import json
import weakref
import asyncio
import logging
import time
from typing import (
AsyncGenerator, Tuple, Any, Dict, Optional, List, Union,
)
from aiohttp.client import ClientResponse
import aiohttputils
logger = logging.getLogger(__name__)
JsonDict = Dict[str, Any]
Json = Union[List[JsonDict], JsonDict]
def parse_datetime(s):
dt = datetime.datetime.strptime(s, '%Y-%m-%dT%H:%M:%SZ')
return dt.replace(tzinfo=datetime.timezone.utc)
class GitHubError(Exception):
def __init__(self, message, documentation, code):
self.message = message
self.documentation = documentation
self.code = code
class GitHub(aiohttputils.ClientBase):
baseurl = 'https://api.github.com/'
def __init__(self, token, session=None):
self.token = f'token {token}'
super().__init__(session = session)
async def api_request(
self, path: str, method: str = 'get',
data: Optional[JsonDict] = None, **kwargs,
) -> Tuple[Json, ClientResponse]:
h = kwargs.get('headers', None)
if not h:
h = kwargs['headers'] = {}
h.setdefault('Accept', 'application/vnd.github.v3+json')
h.setdefault('Authorization', self.token)
if data:
binary_data = json.dumps(data, ensure_ascii=False).encode('utf-8')
if method == 'get':
method = 'post'
h.setdefault('Content-Type', 'application/json')
kwargs['data'] = binary_data
for _ in range(3):
res = await self.request(path, method=method, **kwargs)
j: JsonDict
if res.status == 204:
j = {}
else:
j = await res.json()
if 'message' in j:
if res.status == 403 and int(res.headers.get('X-RateLimit-Remaining', -1)) == 0:
reset = int(res.headers['X-RateLimit-Reset']) - time.time() + 1
logger.warn('rate limited; sleeping for %ds: %s', reset, j['message'])
await asyncio.sleep(reset)
continue
raise GitHubError(j['message'], j['documentation_url'], res.status)
return j, res
raise Exception('unreachable')
async def get_repo_issues(
self, repo: str, *, state: str = 'open', labels: str = '',
) -> AsyncGenerator[Issue, None]:
params = {'state': state}
if labels:
params['labels'] = labels
j, r = await self.api_request(f'/repos/{repo}/issues', params = params)
assert isinstance(j, list)
for x in j:
yield Issue(x, self)
while 'next' in r.links:
url = str(r.links['next']['url'])
j, r = await self.api_request(url)
assert isinstance(j, list)
for x in j:
yield Issue(x, self)
async def get_issue(self, repo: str, issue_nr: int) -> 'Issue':
j, _ = await self.api_request(f'/repos/{repo}/issues/{issue_nr}')
assert isinstance(j, dict)
return Issue(j, self)
async def get_issue_comments(
self, repo: str, issue_nr: int,
) -> AsyncGenerator[Comment, None]:
j, r = await self.api_request(f'/repos/{repo}/issues/{issue_nr}/comments')
assert isinstance(j, list)
for x in j:
yield Comment(x, self)
while 'next' in r.links:
url = str(r.links['next']['url'])
j, r = await self.api_request(url)
assert isinstance(j, list)
for x in j:
yield Comment(x, self)
async def create_issue(
self, repo: str, title: str, body: Optional[str] = None,
labels: List[str] = [],
) -> 'Issue':
data: JsonDict = {
'title': title,
}
if body:
data['body'] = body
if labels:
data['labels'] = labels
issue, _ = await self.api_request(f'/repos/{repo}/issues', data = data)
assert isinstance(issue, dict)
return Issue(issue, self)
async def find_login_by_email(self, email: str) -> str:
j, _ = await self.api_request(f'/search/users?q={email}')
assert isinstance(j, dict)
try:
return j['items'][0]['login']
except IndexError:
raise LookupError(email)
class Issue:
def __init__(self, data: JsonDict, gh: GitHub) -> None:
self.gh = weakref.proxy(gh)
self._data = data
self.body = data['body']
self.number = data['number']
self.title = data['title']
self.labels = [x['name'] for x in data['labels']]
self.updated_at = parse_datetime(data['updated_at'])
self._api_url = f"{data['repository_url']}/issues/{data['number']}"
self.closed = data['state'] == 'closed'
self.author = data['user']['login']
self.closed_by = data.get('closed_by') and data['closed_by']['login'] or None
async def comment(self, comment: str) -> JsonDict:
j, _ = await self.gh.api_request(f'{self._api_url}/comments', data = {'body': comment})
return j
async def add_labels(self, labels: List[str]) -> JsonDict:
j, _ = await self.gh.api_request(f'{self._api_url}/labels', data = labels)
return j
async def assign(self, assignees: List[str]) -> JsonDict:
payload = {'assignees': assignees}
j, _ = await self.gh.api_request(f'{self._api_url}/assignees', data = payload)
return j
async def close(self) -> None:
data, _ = await self.gh.api_request(
f'{self._api_url}', method = 'patch', data = {'state': 'closed'})
self._data = data
self.closed = data['state'] == 'closed'
async def reopen(self) -> None:
data, _ = await self.gh.api_request(
f'{self._api_url}', method = 'patch', data = {'state': 'open'})
self._data = data
self.closed = data['state'] == 'closed'
def __repr__(self):
return f'<Issue {self.number}: {self.title!r}>'
class Comment:
def __init__(self, data: JsonDict, gh: GitHub) -> None:
self.gh = weakref.proxy(gh)
self._update(data)
def _update(self, data: JsonDict) -> None:
self._data = data
self.author = data['user']['login']
self.html_url = data['html_url']
self.url = data['url']
self.body = data['body']
async def delete(self) -> None:
await self.gh.api_request(self.url, method = 'DELETE')
async def edit(self, body: str) -> None:
data, _ = await self.gh.api_request(
self.url, method = 'PATCH',
data = {'body': body},
)
self._update(data)
def __repr__(self) -> str:
return f'<Comment by {self.author}: {self.html_url}>'