mirror of
https://github.com/moparisthebest/xeps
synced 2024-11-28 20:22:24 -05:00
Implement tool to accept ProtoXEPs
This commit is contained in:
parent
6f4924685e
commit
a512dadefa
203
tools/accept.py
Normal file
203
tools/accept.py
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from xeplib import load_xepinfos, Status, choose
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_XEPLIST_PATH = "./build/xeplist.xml"
|
||||||
|
XEP_FILENAME_RE = re.compile(r"xep-(\d+)\.xml")
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
BLANK_NUMBER = re.compile("<number>[xX]{4}</number>")
|
||||||
|
PROTOXEP_STATUS = "<status>ProtoXEP</status>"
|
||||||
|
EXPERIMENTAL_STATUS = "<status>Experimental</status>"
|
||||||
|
REVISION_RE = re.compile(r"\s+<revision>")
|
||||||
|
REVISION_TEMPLATE = """
|
||||||
|
<revision>
|
||||||
|
<version>{version}</version>
|
||||||
|
<date>{now:%Y-%m-%d}</date>
|
||||||
|
<initials>XEP Editor ({initials})</initials>
|
||||||
|
<remark>Accepted by vote of {approving_body} on {date}.</remark>
|
||||||
|
</revision>"""
|
||||||
|
|
||||||
|
|
||||||
|
def accept_xep(number, last_version,
|
||||||
|
initials,
|
||||||
|
approving_body,
|
||||||
|
votedate):
|
||||||
|
filename = "xep-{:04d}.xml".format(number)
|
||||||
|
with open(filename, "r") as f:
|
||||||
|
xep_text = f.read()
|
||||||
|
|
||||||
|
if PROTOXEP_STATUS not in xep_text:
|
||||||
|
raise ValueError("cannot find experimental status in XEP text")
|
||||||
|
|
||||||
|
# this is so incredibly evil ...
|
||||||
|
xep_text = xep_text.replace(PROTOXEP_STATUS, EXPERIMENTAL_STATUS, 1)
|
||||||
|
xep_text, n = BLANK_NUMBER.subn("<number>{:04d}</number>".format(number),
|
||||||
|
xep_text,
|
||||||
|
1)
|
||||||
|
if n == 0:
|
||||||
|
raise ValueError("cannot find number placeholder in XEP text")
|
||||||
|
revision_match = REVISION_RE.search(xep_text)
|
||||||
|
|
||||||
|
version = last_version.split(".")
|
||||||
|
if len(version) == 1:
|
||||||
|
version.append("1")
|
||||||
|
else:
|
||||||
|
version[1] = str(int(version[1]) + 1)
|
||||||
|
del version[2:]
|
||||||
|
|
||||||
|
xep_text = (
|
||||||
|
xep_text[:revision_match.start()] +
|
||||||
|
REVISION_TEMPLATE.format(
|
||||||
|
now=datetime.utcnow(),
|
||||||
|
version=".".join(version),
|
||||||
|
initials=initials,
|
||||||
|
date=votedate,
|
||||||
|
approving_body=approving_body,
|
||||||
|
) + xep_text[revision_match.start():]
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(filename, "w") as f:
|
||||||
|
f.write(xep_text)
|
||||||
|
f.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def isodate(s):
|
||||||
|
return datetime.strptime(s, "%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
|
def get_next_xep_number(accepted):
|
||||||
|
return max(accepted.keys()) + 1
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Accept an inbox XEP."
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-l", "--xeplist",
|
||||||
|
type=argparse.FileType("rb"),
|
||||||
|
default=None,
|
||||||
|
help="XEP list to use (defaults to {})".format(DEFAULT_XEPLIST_PATH)
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-y", "--yes",
|
||||||
|
dest="ask",
|
||||||
|
action="store_false",
|
||||||
|
help="Assume default answer to all questions.",
|
||||||
|
default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"-f", "--force",
|
||||||
|
dest="force",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Force acceptance even if suspicious.",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"item",
|
||||||
|
help="Inbox name"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"votedate",
|
||||||
|
type=isodate,
|
||||||
|
help="The date of the vote, in ISO format (%Y-%m-%d)."
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"initials",
|
||||||
|
help="Your editor initials"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.item.endswith(".xml"):
|
||||||
|
# strip the path
|
||||||
|
p = pathlib.Path(args.item)
|
||||||
|
args.item = p.parts[-1].rsplit(".")[0]
|
||||||
|
|
||||||
|
if args.xeplist is None:
|
||||||
|
args.xeplist = open(DEFAULT_XEPLIST_PATH, "rb")
|
||||||
|
|
||||||
|
if args.xeplist is not None:
|
||||||
|
with args.xeplist as f:
|
||||||
|
tree = etree.parse(f)
|
||||||
|
accepted, inbox = load_xepinfos(tree)
|
||||||
|
|
||||||
|
try:
|
||||||
|
xepinfo = inbox[args.item]
|
||||||
|
except KeyError:
|
||||||
|
print("no such inbox xep: {!r}".format(args.item), file=sys.stderr)
|
||||||
|
print("maybe run make build/xeplist.xml first?", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
new_number = get_next_xep_number(accepted)
|
||||||
|
|
||||||
|
new_filename = pathlib.Path(".") / "xep-{:04d}.xml".format(new_number)
|
||||||
|
inbox_path = pathlib.Path("inbox") / "{}.xml".format(args.item)
|
||||||
|
if new_filename.exists():
|
||||||
|
raise FileExistsError(
|
||||||
|
"Internal error: XEP file does already exist! ({})".format(
|
||||||
|
new_filename
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not inbox_path.exists():
|
||||||
|
print("inbox file does not exist or is not readable: {}".format(
|
||||||
|
inbox_path
|
||||||
|
))
|
||||||
|
|
||||||
|
if args.ask:
|
||||||
|
print("I am going to accept:")
|
||||||
|
print()
|
||||||
|
print(" Title: {!r}".format(xepinfo["title"]))
|
||||||
|
print(" Abstract: {!r}".format(xepinfo["abstract"]))
|
||||||
|
print(" Last Revision: {} ({})".format(
|
||||||
|
xepinfo["last_revision"]["date"].date(),
|
||||||
|
xepinfo["last_revision"]["version"],
|
||||||
|
))
|
||||||
|
print()
|
||||||
|
print("as new XEP-{:04d}.".format(new_number))
|
||||||
|
print()
|
||||||
|
choice = choose("Is this correct? [y]es, [n]o: ", "yn", eof="n")
|
||||||
|
if choice != "y":
|
||||||
|
print("aborted at user request")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
shutil.copy(str(inbox_path), str(new_filename))
|
||||||
|
|
||||||
|
accept_xep(new_number,
|
||||||
|
xepinfo["last_revision"]["version"],
|
||||||
|
args.initials,
|
||||||
|
xepinfo["approver"],
|
||||||
|
args.votedate.date())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@ -12,7 +12,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
import xml.etree.ElementTree as etree
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
from xeplib import Status, Action, load_xepinfos
|
from xeplib import Status, Action, load_xepinfos, choose
|
||||||
|
|
||||||
|
|
||||||
DESCRIPTION = """\
|
DESCRIPTION = """\
|
||||||
@ -279,30 +279,6 @@ def interactively_extend_smtp_config(config):
|
|||||||
config.set("smtp", "from", from_)
|
config.set("smtp", "from", from_)
|
||||||
|
|
||||||
|
|
||||||
def choose(prompt, options, *,
|
|
||||||
eof=EOFError,
|
|
||||||
keyboard_interrupt=KeyboardInterrupt):
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
choice = input(prompt).strip()
|
|
||||||
except EOFError:
|
|
||||||
if eof is EOFError:
|
|
||||||
raise
|
|
||||||
return eof
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
if keyboard_interrupt is KeyboardInterrupt:
|
|
||||||
raise
|
|
||||||
return keyboard_interrupt
|
|
||||||
|
|
||||||
if choice not in options:
|
|
||||||
print("invalid choice. please enter one of: {}".format(
|
|
||||||
", ".join(map(str, options))
|
|
||||||
))
|
|
||||||
continue
|
|
||||||
|
|
||||||
return choice
|
|
||||||
|
|
||||||
|
|
||||||
def make_smtpconn(config):
|
def make_smtpconn(config):
|
||||||
host = config.get("smtp", "host")
|
host = config.get("smtp", "host")
|
||||||
port = config.getint("smtp", "port")
|
port = config.getint("smtp", "port")
|
||||||
|
@ -37,6 +37,7 @@ class Action(enum.Enum):
|
|||||||
UPDATE = "UPDATED"
|
UPDATE = "UPDATED"
|
||||||
DEPRECATE = "DEPRECATED"
|
DEPRECATE = "DEPRECATED"
|
||||||
LAST_CALL = "LAST CALL"
|
LAST_CALL = "LAST CALL"
|
||||||
|
REJECT = "REJECT"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fromstatus(cls, status):
|
def fromstatus(cls, status):
|
||||||
@ -50,6 +51,7 @@ class Action(enum.Enum):
|
|||||||
Status.DEPRECATED: cls.DEPRECATE,
|
Status.DEPRECATED: cls.DEPRECATE,
|
||||||
Status.DEFERRED: cls.DEFER,
|
Status.DEFERRED: cls.DEFER,
|
||||||
Status.PROPOSED: cls.LAST_CALL,
|
Status.PROPOSED: cls.LAST_CALL,
|
||||||
|
Status.REJECTED: cls.REJECT,
|
||||||
}[status]
|
}[status]
|
||||||
|
|
||||||
|
|
||||||
@ -143,3 +145,27 @@ def minidom_children(elem):
|
|||||||
child for child in elem.childNodes
|
child for child in elem.childNodes
|
||||||
if isinstance(child, (xml.dom.minidom.Element))
|
if isinstance(child, (xml.dom.minidom.Element))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def choose(prompt, options, *,
|
||||||
|
eof=EOFError,
|
||||||
|
keyboard_interrupt=KeyboardInterrupt):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
choice = input(prompt).strip()
|
||||||
|
except EOFError:
|
||||||
|
if eof is EOFError:
|
||||||
|
raise
|
||||||
|
return eof
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
if keyboard_interrupt is KeyboardInterrupt:
|
||||||
|
raise
|
||||||
|
return keyboard_interrupt
|
||||||
|
|
||||||
|
if choice not in options:
|
||||||
|
print("invalid choice. please enter one of: {}".format(
|
||||||
|
", ".join(map(str, options))
|
||||||
|
))
|
||||||
|
continue
|
||||||
|
|
||||||
|
return choice
|
||||||
|
Loading…
Reference in New Issue
Block a user