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