From 18fbef73b49236a91005744a30f864f81781c235 Mon Sep 17 00:00:00 2001 From: Jonas Wielicki Date: Wed, 23 Aug 2017 13:47:09 +0200 Subject: [PATCH] tooling: refactor commonly used stuff into xeplib.py --- tools/deferrals.py | 57 ++++++++++++++++ tools/extract-metadata.py | 81 +++++++++------------- tools/send-updates.py | 103 +--------------------------- tools/xeplib.py | 138 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 230 insertions(+), 149 deletions(-) create mode 100755 tools/deferrals.py create mode 100644 tools/xeplib.py diff --git a/tools/deferrals.py b/tools/deferrals.py new file mode 100755 index 00000000..6674385c --- /dev/null +++ b/tools/deferrals.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +import xml.etree.ElementTree as etree + +from datetime import datetime, timedelta + +from xeplib import load_xepinfos, Status + + +def get_deferred(accepted): + now = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) + threshold = now.replace(year=now.year - 1) + + for number, info in sorted(accepted.items()): + if info["status"] == Status.EXPERIMENTAL and "last_revision" in info: + last_update = info["last_revision"]["date"] + if last_update <= threshold: + yield info + + +def main(): + import argparse + + parser = argparse.ArgumentParser( + description="Show the XEPs which need to be changed to deferred." + ) + + parser.add_argument( + "-l", "--xeplist", + type=argparse.FileType("rb"), + default=None, + help="XEP list to use (defaults to ./build/xeplist.xml)" + ) + + parser.add_argument( + "-m", "--modify", + action="store_true", + default=False, + help="Modify the XEP files in-place." + ) + + args = parser.parse_args() + + if args.xeplist is None: + args.xeplist = open("./build/xeplist.xml", "rb") + + with args.xeplist as f: + tree = etree.parse(f) + + accepted, _ = load_xepinfos(tree) + deferred = list(get_deferred(accepted)) + + for deferred_info in deferred: + print(deferred_info["number"]) + + +if __name__ == "__main__": + main() diff --git a/tools/extract-metadata.py b/tools/extract-metadata.py index 3656a7d3..1132c2bb 100755 --- a/tools/extract-metadata.py +++ b/tools/extract-metadata.py @@ -4,6 +4,13 @@ import xml.dom.minidom import xml.etree.ElementTree as etree +from xeplib import ( + minidom_find_child, + minidom_find_header, + minidom_get_text, + minidom_children, +) + DESCRIPTION = """\ Extract a list of XEPs with metadata from the xeps repository.""" @@ -15,53 +22,29 @@ def open_xml(f): return xml.dom.minidom.parse(f) -def find_child(elem, child_tag): - for child in elem.childNodes: - if hasattr(child, "tagName") and child.tagName == child_tag: - return child - return None - - -def find_header(document): - header = find_child(document.documentElement, "header") - if header is None: - raise ValueError("cannot find
") - return header - - -def get_text(elem): - return "".join( - child.nodeValue - for child in elem.childNodes - if isinstance(child, (xml.dom.minidom.Text, - xml.dom.minidom.CDATASection)) - ) - - -def children(elem): - return [ - child for child in elem.childNodes - if isinstance(child, (xml.dom.minidom.Element)) - ] - - def extract_xep_metadata(document): - header = find_header(document) + header = minidom_find_header(document) - latest_revision = find_child(header, "revision") + latest_revision = minidom_find_child(header, "revision") if latest_revision is not None: - last_revision_version = get_text(find_child(latest_revision, "version")) - last_revision_date = get_text(find_child(latest_revision, "date")) - remark_el = find_child(latest_revision, "remark") + last_revision_version = minidom_get_text( + minidom_find_child(latest_revision, "version") + ) + last_revision_date = minidom_get_text( + minidom_find_child(latest_revision, "date") + ) + remark_el = minidom_find_child(latest_revision, "remark") last_revision_remark = None if remark_el is not None: - remark_children = children(remark_el) + remark_children = minidom_children(remark_el) if len(remark_children) == 1 and remark_children[0].tagName == "p": - last_revision_remark = get_text(remark_children[0]) + last_revision_remark = minidom_get_text(remark_children[0]) if last_revision_remark is not None: - initials_el = find_child(latest_revision, "initials") - last_revision_initials = initials_el and get_text(initials_el) + initials_el = minidom_find_child(latest_revision, "initials") + last_revision_initials = initials_el and minidom_get_text( + initials_el + ) else: last_revision_initials = None else: @@ -70,24 +53,26 @@ def extract_xep_metadata(document): last_revision_remark = None last_revision_initials = None - status = get_text(find_child(header, "status")) - type_ = get_text(find_child(header, "type")) - abstract = " ".join(get_text(find_child(header, "abstract")).split()) - sig_el = find_child(header, "sig") + status = minidom_get_text(minidom_find_child(header, "status")) + type_ = minidom_get_text(minidom_find_child(header, "type")) + abstract = " ".join(minidom_get_text( + minidom_find_child(header, "abstract") + ).split()) + sig_el = minidom_find_child(header, "sig") if sig_el is None: sig = None else: - sig = get_text(sig_el) - shortname = get_text(find_child(header, "shortname")) + sig = minidom_get_text(sig_el) + shortname = minidom_get_text(minidom_find_child(header, "shortname")) if shortname.replace("-", " ").replace("_", " ").lower() in [ "not yet assigned", "n/a", "none", "to be assigned", "to be issued"]: shortname = None - title = get_text(find_child(header, "title")) + title = minidom_get_text(minidom_find_child(header, "title")) - approver_el = find_child(header, "approver") + approver_el = minidom_find_child(header, "approver") if approver_el is not None: - approver = get_text(approver_el) + approver = minidom_get_text(approver_el) else: approver = "Board" if type_ == "Procedural" else "Council" diff --git a/tools/send-updates.py b/tools/send-updates.py index 454e7aaa..065e93b3 100755 --- a/tools/send-updates.py +++ b/tools/send-updates.py @@ -3,7 +3,6 @@ import configparser import getpass import itertools import email.message -import enum import os import smtplib import sys @@ -13,6 +12,8 @@ from datetime import datetime import xml.etree.ElementTree as etree +from xeplib import Status, Action, load_xepinfos + DESCRIPTION = """\ Send email updates for XEP changes based on the difference between two \ @@ -35,51 +36,6 @@ standard output are a terminal, the script interactively asks for the option \ values. If no terminal is connected, the script exits with an error instead.""" -class Status(enum.Enum): - PROTO = 'ProtoXEP' - EXPERIMENTAL = 'Experimental' - PROPOSED = 'Proposed' - DRAFT = 'Draft' - ACTIVE = 'Active' - FINAL = 'Final' - RETRACTED = 'Retracted' - OBSOLETE = 'Obsolete' - DEFERRED = 'Deferred' - REJECTED = 'Rejected' - DEPRECATED = 'Deprecated' - - @classmethod - def fromstr(cls, s): - if s == "Proto" or s.lower() == "protoxep": - s = "ProtoXEP" - return cls(s) - - -class Action(enum.Enum): - PROTO = "Proposed XMPP Extension" - NEW = "NEW" - DRAFT = "DRAFT" - ACTIVE = "ACTIVE" - FINAL = "FINAL" - RETRACT = "RETRACTED" - OBSOLETE = "OBSOLETED" - DEFER = "DEFERRED" - UPDATE = "UPDATED" - - @classmethod - def fromstatus(cls, status): - return { - Status.EXPERIMENTAL: cls.NEW, - Status.DRAFT: cls.DRAFT, - Status.ACTIVE: cls.ACTIVE, - Status.FINAL: cls.FINAL, - Status.RETRACTED: cls.RETRACT, - Status.OBSOLETED: cls.OBSOLETE, - Status.DEPRECATED: cls.DEPRECATE, - Status.DEFERRED: cls.DEFERRED, - } - - XEP_URL_PREFIX = "https://xmpp.org/extensions/" @@ -116,61 +72,6 @@ SUBJECT_NONPROTO_TEMPLATE = \ "{action.value}: XEP-{info[number]:04d} ({info[title]})" -def load_xepinfo(el): - accepted = el.get("accepted").lower() == "true" - - info = { - "title": el.find("title").text, - "abstract": el.find("abstract").text, - "type": el.find("type").text, - "status": Status.fromstr(el.find("status").text), - "approver": el.find("approver").text, - "accepted": accepted, - } - - last_revision_el = el.find("last-revision") - if last_revision_el is not None: - last_revision = { - "version": last_revision_el.find("version").text, - "date": last_revision_el.find("date").text, - "initials": None, - "remark": None, - } - - initials_el = last_revision_el.find("initials") - if initials_el is not None: - last_revision["initials"] = initials_el.text - - remark_el = last_revision_el.find("remark") - if remark_el is not None: - last_revision["remark"] = remark_el.text - - info["last_revision"] = last_revision - - sig = el.find("sig") - if sig is not None: - info["sig"] = sig.text - - if accepted: - info["number"] = int(el.find("number").text) - else: - info["protoname"] = el.find("proto-name").text - - return info - - -def load_xepinfos(tree): - accepted, protos = {}, {} - for info_el in tree.getroot(): - info = load_xepinfo(info_el) - if info["accepted"]: - accepted[info["number"]] = info - else: - protos[info["protoname"]] = info - - return accepted, protos - - def dummy_info(number): return { "status": None, diff --git a/tools/xeplib.py b/tools/xeplib.py new file mode 100644 index 00000000..5a250041 --- /dev/null +++ b/tools/xeplib.py @@ -0,0 +1,138 @@ +import enum + +import xml.dom.minidom + +from datetime import datetime + + +class Status(enum.Enum): + PROTO = 'ProtoXEP' + EXPERIMENTAL = 'Experimental' + PROPOSED = 'Proposed' + DRAFT = 'Draft' + ACTIVE = 'Active' + FINAL = 'Final' + RETRACTED = 'Retracted' + OBSOLETE = 'Obsolete' + DEFERRED = 'Deferred' + REJECTED = 'Rejected' + DEPRECATED = 'Deprecated' + + @classmethod + def fromstr(cls, s): + if s == "Proto" or s.lower() == "protoxep": + s = "ProtoXEP" + return cls(s) + + +class Action(enum.Enum): + PROTO = "Proposed XMPP Extension" + NEW = "NEW" + DRAFT = "DRAFT" + ACTIVE = "ACTIVE" + FINAL = "FINAL" + RETRACT = "RETRACTED" + OBSOLETE = "OBSOLETED" + DEFER = "DEFERRED" + UPDATE = "UPDATED" + + @classmethod + def fromstatus(cls, status): + return { + Status.EXPERIMENTAL: cls.NEW, + Status.DRAFT: cls.DRAFT, + Status.ACTIVE: cls.ACTIVE, + Status.FINAL: cls.FINAL, + Status.RETRACTED: cls.RETRACT, + Status.OBSOLETED: cls.OBSOLETE, + Status.DEPRECATED: cls.DEPRECATE, + Status.DEFERRED: cls.DEFERRED, + }[status] + + +def load_xepinfo(el): + accepted = el.get("accepted").lower() == "true" + + info = { + "title": el.find("title").text, + "abstract": el.find("abstract").text, + "type": el.find("type").text, + "status": Status.fromstr(el.find("status").text), + "approver": el.find("approver").text, + "accepted": accepted, + } + + last_revision_el = el.find("last-revision") + if last_revision_el is not None: + last_revision = { + "version": last_revision_el.find("version").text, + "date": datetime.strptime( + last_revision_el.find("date").text, + "%Y-%m-%d", + ), + "initials": None, + "remark": None, + } + + initials_el = last_revision_el.find("initials") + if initials_el is not None: + last_revision["initials"] = initials_el.text + + remark_el = last_revision_el.find("remark") + if remark_el is not None: + last_revision["remark"] = remark_el.text + + info["last_revision"] = last_revision + + sig = el.find("sig") + if sig is not None: + info["sig"] = sig.text + + if accepted: + info["number"] = int(el.find("number").text) + else: + info["protoname"] = el.find("proto-name").text + + return info + + +def load_xepinfos(tree): + accepted, protos = {}, {} + for info_el in tree.getroot(): + info = load_xepinfo(info_el) + if info["accepted"]: + accepted[info["number"]] = info + else: + protos[info["protoname"]] = info + + return accepted, protos + + +def minidom_find_child(elem, child_tag): + for child in elem.childNodes: + if hasattr(child, "tagName") and child.tagName == child_tag: + return child + return None + + +def minidom_find_header(document): + header = minidom_find_child(document.documentElement, "header") + if header is None: + raise ValueError("cannot find
") + return header + + +def minidom_get_text(elem): + return "".join( + child.nodeValue + for child in elem.childNodes + if isinstance(child, (xml.dom.minidom.Text, + xml.dom.minidom.CDATASection)) + ) + + +def minidom_children(elem): + return [ + child for child in elem.childNodes + if isinstance(child, (xml.dom.minidom.Element)) + ]