github.py (5001B)
1 # This Source Code Form is subject to the terms of the Mozilla Public 2 # License, v. 2.0. If a copy of the MPL was not distributed with this 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5 import json 6 7 from six.moves.urllib.parse import urljoin 8 9 requests = None 10 11 12 class GitHubError(Exception): 13 def __init__(self, status, data): 14 self.status = status 15 self.data = data 16 17 18 class GitHub: 19 url_base = "https://api.github.com" 20 21 def __init__(self, token): 22 # Defer the import of requests since it isn't installed by default 23 global requests 24 if requests is None: 25 import requests 26 27 self.headers = {"Accept": "application/vnd.github.v3+json"} 28 self.auth = (token, "x-oauth-basic") 29 30 def get(self, path): 31 return self._request("GET", path) 32 33 def post(self, path, data): 34 return self._request("POST", path, data=data) 35 36 def put(self, path, data, headers=None): 37 return self._request("PUT", path, data=data, headers=headers) 38 39 def _request(self, method, path, data=None, headers=None): 40 url = urljoin(self.url_base, path) 41 42 headers_ = self.headers 43 if headers is not None: 44 headers_.update(headers) 45 kwargs = {"headers": headers_, "auth": self.auth} 46 if data is not None: 47 kwargs["data"] = json.dumps(data) 48 49 resp = requests.request(method, url, **kwargs) 50 51 if 200 <= resp.status_code < 300: 52 return resp.json() 53 else: 54 print(method, path, resp.status_code, resp.json()) 55 raise GitHubError(resp.status_code, resp.json()) 56 57 def repo(self, owner, name): 58 """GitHubRepo for a particular repository. 59 60 :param owner: String repository owner 61 :param name: String repository name 62 """ 63 return GitHubRepo.from_name(self, owner, name) 64 65 66 class GitHubRepo: 67 def __init__(self, github, data): 68 """Object representing a GitHub repository""" 69 self.gh = github 70 self.owner = data["owner"] 71 self.name = data["name"] 72 self.url = data["ssh_url"] 73 self._data = data 74 75 @classmethod 76 def from_name(cls, github, owner, name): 77 data = github.get("/repos/%s/%s" % (owner, name)) 78 return cls(github, data) 79 80 @property 81 def url_base(self): 82 return "/repos/%s/" % (self._data["full_name"]) 83 84 def create_pr(self, title, head, base, body): 85 """Create a Pull Request in the repository 86 87 :param title: Pull Request title 88 :param head: ref to the HEAD of the PR branch. 89 :param base: ref to the base branch for the Pull Request 90 :param body: Description of the PR 91 """ 92 return PullRequest.create(self, title, head, base, body) 93 94 def load_pr(self, number): 95 """Load an existing Pull Request by number. 96 97 :param number: Pull Request number 98 """ 99 return PullRequest.from_number(self, number) 100 101 def path(self, suffix): 102 return urljoin(self.url_base, suffix) 103 104 105 class PullRequest: 106 def __init__(self, repo, data): 107 """Object representing a Pull Request""" 108 109 self.repo = repo 110 self._data = data 111 self.number = data["number"] 112 self.title = data["title"] 113 self.base = data["base"]["ref"] 114 self.base = data["head"]["ref"] 115 self._issue = None 116 117 @classmethod 118 def from_number(cls, repo, number): 119 data = repo.gh.get(repo.path("pulls/%i" % number)) 120 return cls(repo, data) 121 122 @classmethod 123 def create(cls, repo, title, head, base, body): 124 data = repo.gh.post( 125 repo.path("pulls"), 126 {"title": title, "head": head, "base": base, "body": body}, 127 ) 128 return cls(repo, data) 129 130 def path(self, suffix): 131 return urljoin(self.repo.path("pulls/%i/" % self.number), suffix) 132 133 @property 134 def issue(self): 135 """Issue related to the Pull Request""" 136 if self._issue is None: 137 self._issue = Issue.from_number(self.repo, self.number) 138 return self._issue 139 140 def merge(self): 141 """Merge the Pull Request into its base branch.""" 142 self.repo.gh.put( 143 self.path("merge"), 144 {"merge_method": "merge"}, 145 headers={"Accept": "application/vnd.github.polaris-preview+json"}, 146 ) 147 148 149 class Issue: 150 def __init__(self, repo, data): 151 """Object representing a GitHub Issue""" 152 self.repo = repo 153 self._data = data 154 self.number = data["number"] 155 156 @classmethod 157 def from_number(cls, repo, number): 158 data = repo.gh.get(repo.path("issues/%i" % number)) 159 return cls(repo, data) 160 161 def path(self, suffix): 162 return urljoin(self.repo.path("issues/%i/" % self.number), suffix) 163 164 def add_comment(self, message): 165 """Add a comment to the issue 166 167 :param message: The text of the comment 168 """ 169 self.repo.gh.post(self.path("comments"), {"body": message})