github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/gubernator/github/models.py (about) 1 # Copyright 2016 The Kubernetes Authors. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 import logging 16 import datetime 17 import json 18 19 import google.appengine.ext.ndb as ndb 20 21 22 class GithubResource(ndb.Model): 23 # A key holder used to define an entitygroup for 24 # each Issue/PR, for easy ancestor queries. 25 @staticmethod 26 def make_key(repo, number): 27 return ndb.Key(GithubResource, '%s %s' % (repo, number)) 28 29 30 def shrink(body): 31 """Recursively remove Github API urls from an object to make it more human-readable.""" 32 toremove = [] 33 for key, value in body.iteritems(): 34 if isinstance(value, basestring): 35 if key.endswith('url'): 36 if (value.startswith('https://api.github.com/') or 37 value.startswith('https://avatars.githubusercontent.com')): 38 toremove.append(key) 39 elif isinstance(value, dict): 40 shrink(value) 41 elif isinstance(value, list): 42 for el in value: 43 if isinstance(el, dict): 44 shrink(el) 45 for key in toremove: 46 body.pop(key) 47 return body 48 49 50 class GithubWebhookRaw(ndb.Model): 51 repo = ndb.StringProperty() 52 number = ndb.IntegerProperty(indexed=False) 53 event = ndb.StringProperty() 54 timestamp = ndb.DateTimeProperty(auto_now_add=True) 55 body = ndb.TextProperty(compressed=True) 56 57 def to_tuple(self): 58 return (self.event, shrink(json.loads(self.body)), float(self.timestamp.strftime('%s.%f'))) 59 60 61 def from_iso8601(t): 62 if not t: 63 return t 64 if t.endswith('Z'): 65 return datetime.datetime.strptime(t, '%Y-%m-%dT%H:%M:%SZ') 66 elif t.endswith('+00:00'): 67 return datetime.datetime.strptime(t, '%Y-%m-%dT%H:%M:%S+00:00') 68 else: 69 logging.warning('unparseable time value: %s', t) 70 return None 71 72 73 def make_kwargs(body, fields): 74 kwargs = {} 75 for field in fields: 76 if field.endswith('_at'): 77 kwargs[field] = from_iso8601(body[field]) 78 else: 79 kwargs[field] = body[field] 80 return kwargs 81 82 83 class GHStatus(ndb.Model): 84 # Key: {repo}\t{sha}\t{context} 85 state = ndb.StringProperty(indexed=False) 86 target_url = ndb.StringProperty(indexed=False) 87 description = ndb.TextProperty() 88 89 created_at = ndb.DateTimeProperty(indexed=False) 90 updated_at = ndb.DateTimeProperty(indexed=False) 91 92 93 @staticmethod 94 def make_key(repo, sha, context): 95 return ndb.Key(GHStatus, '%s\t%s\t%s' % (repo, sha, context)) 96 97 @staticmethod 98 def make(repo, sha, context, **kwargs): 99 return GHStatus(key=GHStatus.make_key(repo, sha, context), **kwargs) 100 101 @staticmethod 102 def query_for_sha(repo, sha): 103 before = GHStatus.make_key(repo, sha, '') 104 after = GHStatus.make_key(repo, sha, '\x7f') 105 return GHStatus.query(GHStatus.key > before, GHStatus.key < after) 106 107 @staticmethod 108 def from_json(body): 109 kwargs = make_kwargs(body, 110 'sha context state target_url description ' 111 'created_at updated_at'.split()) 112 kwargs['repo'] = body['name'] 113 return GHStatus.make(**kwargs) 114 115 @property 116 def repo(self): 117 return self.key.id().split('\t', 1)[0] 118 119 @property 120 def sha(self): 121 return self.key.id().split('\t', 2)[1] 122 123 @property 124 def context(self): 125 return self.key.id().split('\t', 2)[2] 126 127 128 class GHIssueDigest(ndb.Model): 129 # Key: {repo} {number} 130 is_pr = ndb.BooleanProperty() 131 is_open = ndb.BooleanProperty() 132 involved = ndb.StringProperty(repeated=True) 133 xref = ndb.StringProperty(repeated=True) 134 payload = ndb.JsonProperty() 135 updated_at = ndb.DateTimeProperty() 136 head = ndb.StringProperty() 137 138 @staticmethod 139 def make_key(repo, number): 140 return ndb.Key(GHIssueDigest, '%s %s' % (repo, number)) 141 142 @staticmethod 143 def make(repo, number, is_pr, is_open, involved, payload, updated_at): 144 return GHIssueDigest(key=GHIssueDigest.make_key(repo, number), 145 is_pr=is_pr, is_open=is_open, involved=involved, payload=payload, 146 updated_at=updated_at, head=payload.get('head'), 147 xref=payload.get('xrefs', [])) 148 149 @staticmethod 150 def get(repo, number): 151 return GHIssueDigest.make_key(repo, number).get() 152 153 @property 154 def repo(self): 155 return self.key.id().split()[0] 156 157 @property 158 def number(self): 159 return int(self.key.id().split()[1]) 160 161 @property 162 def url(self): 163 return 'https://github.com/%s/issues/%s' % tuple(self.key.id().split()) 164 165 @property 166 def title(self): 167 return self.payload.get('title', '') 168 169 @staticmethod 170 def find_head(repo, head): 171 return GHIssueDigest.query(GHIssueDigest.key > GHIssueDigest.make_key(repo, ''), 172 GHIssueDigest.key < GHIssueDigest.make_key(repo, '~'), 173 GHIssueDigest.head == head) 174 175 @staticmethod 176 @ndb.tasklet 177 def find_xrefs_async(xref): 178 issues = yield GHIssueDigest.query(GHIssueDigest.xref == xref).fetch_async() 179 raise ndb.Return(list(issues)) 180 181 @staticmethod 182 @ndb.tasklet 183 def find_xrefs_multi_async(xrefs): 184 """ 185 Given a list of xrefs to search for, return a dict of lists 186 of result values. Xrefs that have no corresponding issues are 187 not represented in the dictionary. 188 """ 189 # The IN operator does multiple sequential queries and ORs them 190 # together. This is slow here-- a range query is faster, since 191 # this is used to get xrefs for a set of contiguous builds. 192 if not xrefs: # nothing => nothing 193 raise ndb.Return({}) 194 xrefs = set(xrefs) 195 issues = yield GHIssueDigest.query( 196 GHIssueDigest.xref >= min(xrefs), 197 GHIssueDigest.xref <= max(xrefs)).fetch_async(batch_size=500) 198 refs = {} 199 for issue in issues: 200 for xref in issue.xref: 201 if xref in xrefs: 202 refs.setdefault(xref, []).append(issue) 203 raise ndb.Return(refs) 204 205 @staticmethod 206 def find_open_prs(): 207 # pylint: disable=singleton-comparison 208 return GHIssueDigest.query(GHIssueDigest.is_pr == True, 209 GHIssueDigest.is_open == True) 210 211 @staticmethod 212 def find_open_prs_for_repo(repo): 213 return (GHIssueDigest.find_open_prs() 214 .filter(GHIssueDigest.key > GHIssueDigest.make_key(repo, ''), 215 GHIssueDigest.key < GHIssueDigest.make_key(repo, '~'))) 216 217 218 class GHUserState(ndb.Model): 219 # Key: {github username} 220 acks = ndb.JsonProperty() # dict of issue keys => ack time (seconds since epoch) 221 222 @staticmethod 223 def make_key(user): 224 return ndb.Key(GHUserState, user) 225 226 @staticmethod 227 def make(user, acks=None): 228 return GHUserState(key=GHUserState.make_key(user), acks=acks or {}) 229 230 231 @ndb.transactional 232 def save_if_newer(obj): 233 assert obj.updated_at is not None 234 old = obj.key.get() 235 if old is None: 236 obj.put() 237 return True 238 else: 239 if old.updated_at is None or obj.updated_at >= old.updated_at: 240 obj.put() 241 return True 242 return False