crashclouseau/buildhub.py (215 lines of code) (raw):

# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. import asyncio from copy import deepcopy from functools import partial import json import requests import six import time from . import utils from .logger import logger URL = "https://buildhub.moz.tools/api/search" PRODS = {"Firefox": "firefox", "FennecAndroid": "fennec", "Thunderbird": "thunderbird"} RPRODS = {v: k for k, v in PRODS.items()} # regexp matching the correct version formats for elastic search query VERSION_PATS = { "nightly": r'[0-9]+".0a1"', "beta": r'[0-9]+".0b"[0-9]+', "release": r"[0-9]+\.[0-9]+(\.[0-9]+)?", } def make_request(params, sleep, retry, callback): """Query Buildhub""" params = json.dumps(params) for _ in range(retry): r = requests.post(URL, data=params) if "Backoff" in r.headers: time.sleep(sleep) else: try: return callback(r.json()) except BaseException as e: logger.error( "Buildhub query failed with parameters: {}.".format(params) ) logger.error(e, exc_info=True) return None logger.error("Too many attempts in buildhub.make_request (retry={})".format(retry)) return None def get( min_buildid, channel, prods=["firefox", "fennec", "thunderbird"], max_buildid=None ): """Get all builds info for buildids >= min_build""" if isinstance(prods, six.string_types): prods = [prods] prods = [PRODS.get(x, x) for x in prods] r = {} if min_buildid: r["gte"] = utils.get_buildid(min_buildid) if max_buildid: r["lte"] = utils.get_buildid(max_buildid) data = { "aggs": { "products": { "terms": {"field": "source.product", "size": len(prods)}, "aggs": { "channels": { "terms": {"field": "target.channel", "size": 1}, "aggs": { "buildids": { "terms": {"field": "build.id", "size": 1000}, "aggs": { "revisions": { "terms": {"field": "source.revision", "size": 1} }, "versions": { "terms": {"field": "target.version", "size": 1} }, }, } }, } }, } }, "query": { "bool": { "filter": [ {"term": {"target.channel": channel}}, {"terms": {"source.product": prods}}, {"range": {"build.id": r}}, {"regexp": {"target.version": VERSION_PATS.get(channel, "*")}}, ] } }, "size": 0, } def get_info(data): res = {} aggs = data["aggregations"] for product in aggs["products"]["buckets"]: prod = product["key"] prod = RPRODS.get(prod, prod) if prod in res: res_p = res[prod] else: res[prod] = res_p = {} for channel in product["channels"]["buckets"]: chan = channel["key"] if chan in res_p: res_pc = res_p[chan] else: res_p[chan] = res_pc = {} for buildid in channel["buildids"]["buckets"]: bid = utils.get_build_date(buildid["key"]) rev = buildid["revisions"]["buckets"][0]["key"] version = buildid["versions"]["buckets"][0]["key"] res_pc[bid] = {"revision": utils.short_rev(rev), "version": version} return res return make_request(data, 1, 100, get_info) def get_rev_from(buildid, channel, product): """Get the revision for a given build""" buildid = utils.get_buildid(buildid) product = PRODS.get(product, product) data = { "aggs": {"revisions": {"terms": {"field": "source.revision", "size": 1}}}, "query": { "bool": { "filter": [ {"term": {"target.channel": channel}}, {"term": {"source.product": product}}, {"term": {"build.id": buildid}}, ] } }, "size": 0, } def cb(data): return utils.short_rev(data["aggregations"]["revisions"]["buckets"][0]["key"]) return make_request(data, 0.1, 100, cb) def get_two_last(buildid, channel, product): """Get the two last build (including the one from buildid)""" buildid = utils.get_buildid(buildid) product = PRODS.get(product, product) data = { "aggs": { "buildids": { "terms": {"field": "build.id", "size": 2, "order": {"_term": "desc"}}, "aggs": { "revisions": {"terms": {"field": "source.revision", "size": 1}}, "versions": {"terms": {"field": "target.version", "size": 1}}, }, } }, "query": { "bool": { "filter": [ {"term": {"target.channel": channel}}, {"term": {"source.product": product}}, {"range": {"build.id": {"lte": buildid}}}, {"regexp": {"target.version": VERSION_PATS.get(channel, "*")}}, ] } }, "size": 0, } def get_info(data): bids = [] for i in data["aggregations"]["buildids"]["buckets"]: bid = i["key"] revision = utils.short_rev(i["revisions"]["buckets"][0]["key"]) version = i["versions"]["buckets"][0]["key"] bids.append({"buildid": bid, "revision": revision, "version": version}) if bids[0]["buildid"] != buildid: return None x = bids[0] bids[0] = bids[1] bids[1] = x return bids return make_request(data, 0.1, 100, get_info) async def get_enclosing_builds_helper(pushdate, channel, product): # TODO: we must handle the case where the timezone of buildid was not utc # check with jlorenzo when the changed has been made buildid = utils.get_buildid(pushdate) product = PRODS.get(product, product) lt_data = { "aggs": { "buildids": { "terms": {"field": "build.id", "size": 1, "order": {"_term": "desc"}}, "aggs": { "revisions": {"terms": {"field": "source.revision", "size": 1}}, "versions": {"terms": {"field": "target.version", "size": 1}}, }, } }, "query": { "bool": { "filter": [ {"term": {"target.channel": channel}}, {"term": {"source.product": product}}, {"range": {"build.id": {"lt": buildid}}}, {"regexp": {"target.version": VERSION_PATS.get(channel, "*")}}, ] } }, "size": 0, } gte_data = deepcopy(lt_data) gte_data["aggs"]["buildids"]["terms"]["order"]["_term"] = "asc" gte_data["query"]["bool"]["filter"][2]["range"]["build.id"] = {"gte": buildid} data = [lt_data, gte_data] def get_info(data): data = data["aggregations"]["buildids"]["buckets"] if len(data) == 0: return None data = data[0] bid = data["key"] revision = utils.short_rev(data["revisions"]["buckets"][0]["key"]) version = data["versions"]["buckets"][0]["key"] return {"buildid": bid, "revision": revision, "version": version} loop = asyncio.get_event_loop() fs = [] for d in data: fs.append( loop.run_in_executor(None, partial(make_request, d, 0.1, 100, get_info)) ) res = [] for f in fs: res.append(await f) return res def get_enclosing_builds(pushdate, channel, product): """Get the build before and the one after the given pushdate""" loop = asyncio.get_event_loop() return loop.run_until_complete( get_enclosing_builds_helper(pushdate, channel, product) )