This is an automated email from the ASF dual-hosted git repository. andschwa pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mesos.git
commit 21bddc30d756ae673a062c7cf23c0f064bafb275 Author: Dragos Schebesch <[email protected]> AuthorDate: Tue Aug 21 10:42:46 2018 -0700 Refactored ReviewBoard API functionality into separate module. Review: https://reviews.apache.org/r/67502/ --- support/python3/common.py | 176 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/support/python3/common.py b/support/python3/common.py new file mode 100644 index 0000000..ed62c67 --- /dev/null +++ b/support/python3/common.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +These common helper classes help to manage the connection between the +machine and ReviewBoard. +""" + +from datetime import datetime +import json +import sys +import urllib.request as urllib2 +from urllib.parse import urlencode + +REVIEWBOARD_URL = "https://reviews.apache.org" + + +class ReviewError(Exception): + """Custom exception raised when a review is bad""" + pass + + +class ReviewBoardHandler(object): + """Handler class for ReviewBoard API operations.""" + + def __init__(self, user=None, password=None): + self.user = user + self.password = password + self._opener_installed = False + + def _review_ids(self, review_request, review_ids=None): + """Helper function for the 'get_review_ids' method.""" + if review_ids is None: + review_ids = [] + if review_request["status"] != "submitted": + review_ids.append(review_request["id"]) + else: + print("The review request %s is already " + "submitted" % (review_request["id"])) + for review in review_request["depends_on"]: + review_url = review["href"] + print("Dependent review: %s" % review_url) + dependent_review = self.api(review_url)["review_request"] + if dependent_review["id"] in review_ids: + raise ReviewError("Circular dependency detected for " + "review %s. Please fix the 'depends_on' " + "field." % review_request["id"]) + self._review_ids(dependent_review, review_ids) + + def api(self, url, data=None): + """Calls the ReviewBoard API.""" + if self._opener_installed is False: + auth_handler = urllib2.HTTPBasicAuthHandler() + auth_handler.add_password( + realm="Web API", + uri="reviews.apache.org", + user=self.user, + passwd=self.password) + opener = urllib2.build_opener(auth_handler) + urllib2.install_opener(opener) + self._opener_installed = True + if data is not None: + data = data.encode(sys.getdefaultencoding()) + try: + return json.loads(urllib2.urlopen(url, data=data).read().decode( + sys.getdefaultencoding())) + except Exception as err: + print("Error handling URL %s: %s" % (url, err)) + # raise the error after printing the message + raise + + def get_dependent_review_ids(self, review_request): + """Returns the review requests' ids (together with any potential + dependent review requests' ids) that need to be applied for the + current review request. Their order is ascending with respect to + how they should be applied. This function raises a ReviewError + exception if a cyclic dependency is found.""" + review_ids = [] + self._review_ids(review_request, review_ids) + return list(reversed(review_ids)) + + def post_review(self, review_request, message, text_type='markdown'): + """Posts a review on the review board.""" + valid_text_types = ['markdown', 'plain'] + if text_type not in valid_text_types: + raise Exception("Invalid %s text type when trying" + " to post review. Valid text" + " types are: %s" % (text_type, valid_text_types)) + review_request_url = "%s/r/%s" % (REVIEWBOARD_URL, + review_request['id']) + print("Posting to review request: %s\n%s" % (review_request_url, + message)) + review_url = review_request["links"]["reviews"]["href"] + data = urlencode({'body_top': message, + 'body_top_text_type': text_type, + 'public': 'true'}) + self.api(review_url, data) + + def needs_verification(self, review_request): + """Returns True if this review request needs to be verified.""" + print("Checking if review %s needs verification" % ( + review_request["id"])) + rb_date_format = "%Y-%m-%dT%H:%M:%SZ" + + # Now apply this review if not yet submitted. + if review_request["status"] == "submitted": + print("The review is already submitted") + return False + + # Skip if the review blocks another review. + if review_request["blocks"]: + print("Skipping blocking review %s" % review_request["id"]) + return False + + # Get the timestamp of the latest review from this script. + reviews_url = review_request["links"]["reviews"]["href"] + reviews = self.api(reviews_url + "?max-results=200") + review_time = None + for review in reversed(reviews["reviews"]): + if review["links"]["user"]["title"] == self.user: + timestamp = review["timestamp"] + review_time = datetime.strptime(timestamp, rb_date_format) + print("Latest review timestamp: %s" % review_time) + break + if not review_time: + # Never reviewed, the review request needs to be verified. + print("Patch never verified, needs verification") + return True + + # Every patch must have a diff. + latest_diff = self.api(review_request["links"]["diffs"]["href"]) + + # Get the timestamp of the latest diff. + timestamp = latest_diff["diffs"][-1]["timestamp"] + diff_time = datetime.strptime(timestamp, rb_date_format) + print("Latest diff timestamp: %s" % diff_time) + + # NOTE: We purposefully allow the bot to run again on empty reviews + # so that users can re-trigger the build. + if review_time < diff_time: + # There is a new diff, needs verification. + print("This patch has been updated since its last review, needs" + " verification.") + return True + + # TODO(dragoshsch): Apply this check recursively up the dependency + # chain. + changes_url = review_request["links"]["changes"]["href"] + changes = self.api(changes_url) + dependency_time = None + for change in changes["changes"]: + if "depends_on" in change["fields_changed"]: + timestamp = change["timestamp"] + dependency_time = datetime.strptime(timestamp, rb_date_format) + print("Latest dependency change timestamp: %s" % + dependency_time) + break + + # Needs verification if there is a new diff, or if the + # dependencies changed, after the last time it was verified. + return dependency_time and review_time < dependency_time
