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))
+ ]