#!/usr/bin/env python3 # -*- coding: UTF-8 -*- """ script for publishing a report to BAT """ # This is part of a gunicorn app (not intended for standalone use) import os import json import requests import re import pdata import committee_info BAT_BASE = 'https://agenda.apache.org' # read-only URLs BAT_CALENDAR = BAT_BASE + '/integration/reporter/calendar.json' BAT_COMMENTS = BAT_BASE + '/integration/reporter/historical-comments' BAT_AGENDA_IP = BAT_BASE + '/integration/reporter/agenda/%s.json' # Submit the report BAT_SUBMIT = BAT_BASE + '/integration/reporter/post' def get_bat(url, env, ttl = 3600): cached = True xurl = re.sub(r"[^a-z0-9]+", "-", url.replace('.json', '')) wanted_file = '/tmp/%s.json' % xurl if pdata.has_cache(wanted_file, ttl = ttl): js = json.load(open(wanted_file)) else: try: print("Fetching %s => %s..." % (url, wanted_file)) rv = requests.get(url, headers = {'Authorization': env.get('HTTP_AUTHORIZATION')}, timeout = 5) rv.raise_for_status() js = rv.json() with open(wanted_file, "w") as f: json.dump(js, f) f.close() cached = False except Exception as e: # fall back to cache on failure! print(f"Failed to fetch {url}: {e}") if os.path.exists(wanted_file): print(f"Using stale cache for {wanted_file}") js = json.load(open(wanted_file)) else: print(f"No cache for {wanted_file}") raise return js, cached def latest_agenda(environ): calendar, _cached = get_bat(BAT_CALENDAR, environ) latest = calendar['agendas'][-1] ymd = re.match(r"board_agenda_(\d\d\d\d_\d\d_\d\d)\.txt", latest) if ymd: return latest, BAT_AGENDA_IP % ymd.group(1).replace('_', '-') else: return latest, BAT_AGENDA_IP % 'latest' def has_access(user, project): member = pdata.isASFMember(user) pmc = project in pdata.getPMCs(user) return (member or pmc) def guess_title(project): """ Guess the whimsy name of a project """ pmcSummary = committee_info.PMCsummary() # Figure out the name as written in whimsy.. pname = project if project in pmcSummary: pname = pmcSummary[project]['name'].replace('Apache ', '') return pname # /api/bat/agenda/refresh def agenda_forced(environ, user): """ Force BAT agenda refresh... """ _txtfile, url = latest_agenda(environ) get_bat(url, environ, ttl = 0) return agenda(environ, user) # /api/bat/agenda def agenda(environ, user, fproject = None): """ Returns data on the board report for a project, IF present and/or filed in the current agenda """ project = fproject or environ.get('QUERY_STRING') report = None _txtfile, url = latest_agenda(environ) if has_access(user, project): agenda, _cached = get_bat(url, environ) for entry in agenda: # Ideally the agenda should include the bare project id, but it does not # So we get it from the roster (could also use stats) rid = entry.get('roster', '').replace('https://whimsy.apache.org/roster/committee/', '') if project == rid: report = entry break comments, _cached = get_bat(BAT_COMMENTS, environ) title = report and report.get('title', guess_title(project)) if title in comments: comments = comments[title] else: comments = {} # Allow for empty agenda response if len(agenda) > 0: timestamp = agenda[0].get('timestamp', 0) else: timestamp = 0 return { 'can_access': True, 'found': report and True or False, 'filed': report and report.get('report') and True or False, 'report': report, 'comments': comments, 'timestamp': timestamp, } return { 'can_access': False, 'found': False, } # /api/bat/comments def comments(environ, user): """ Display board feedback from previous reports ... """ project = environ.get('QUERY_STRING') comments, cached = get_bat(BAT_COMMENTS, environ) # Figure out the name as written in whimsy.. pname = guess_title(project) # If we can access, fetch comments if comments and pname in comments and has_access(user, project): comments = comments[pname] else: comments = {} js = { "pid": project, "pname": pname, "comments": comments, "used_cache": cached, } return js # /api/bat/publish def publish(environ, _user): try: request_body_size = int(environ.get('CONTENT_LENGTH', 0)) except (ValueError): request_body_size = 0 if request_body_size: request_body = environ['wsgi.input'].read(request_body_size) try: js = json.loads(request_body.decode('utf-8')) except: js = {} if js: agenda = js.get('agenda') project = js.get('project') report = js.get('report') digest = js.get('digest') attach = js.get('attach') print(project, agenda) if agenda and project and report: message = "Publishing report for %s via Reporter" % project payload = { 'agenda': agenda, 'project': project, 'report': report, 'message': message, } if digest and attach: del payload['project'] payload['attach'] = attach payload['message'] = "Updating report for %s via Reporter." % project payload['digest'] = digest try: rv = requests.post(BAT_SUBMIT, headers = { 'Authorization': environ.get('HTTP_AUTHORIZATION'), "Content-Type": "application/json" }, json = payload, timeout = 10) # Intercept client errors if 400 <= rv.status_code < 500: return {'okay': False, 'message': rv.text} rv.raise_for_status() return {'okay': True, 'message': "Posted to board agenda!"} except: pass return {}