diff --git a/bikeshed/cli.py b/bikeshed/cli.py index 464d73e501..e880e7c550 100644 --- a/bikeshed/cli.py +++ b/bikeshed/cli.py @@ -197,6 +197,13 @@ def main() -> None: help="Don't actually submit to the echidna service, instead just echo the prepared TAR file to stdout.", ) + w3cParser = subparsers.add_parser("w3c", help="Commands to interact with W3C services.") + w3cParser.add_argument( + "--pub-status", + dest="pubToken", + help="Check the status of an Echidna publication. Pass the publication id.", + ) + watchParser = subparsers.add_parser( "watch", help="Process a spec source file into a valid output file, automatically rebuilding when it changes.", @@ -483,6 +490,8 @@ def main() -> None: handleTemplate() elif options.subparserName == "wpt": handleWpt(options) + elif options.subparserName == "w3c": + handleW3c(options) def handleUpdate(options: argparse.Namespace) -> None: @@ -582,6 +591,13 @@ def handleServe(options: argparse.Namespace, extras: list[str]) -> None: doc.watch(outputFilename=options.outfile, port=int(options.port)) +def handleW3c(options: argparse.Namespace) -> None: + from . import w3c + + if options.pubToken: + w3c.checkEchidna(options.pubToken) + + def handleDebug(options: argparse.Namespace, extras: list[str]) -> None: from . import metadata from .Spec import Spec diff --git a/bikeshed/w3c/__init__.py b/bikeshed/w3c/__init__.py new file mode 100644 index 0000000000..055968b815 --- /dev/null +++ b/bikeshed/w3c/__init__.py @@ -0,0 +1 @@ +from .echidna import checkEchidna diff --git a/bikeshed/w3c/echidna.py b/bikeshed/w3c/echidna.py new file mode 100644 index 0000000000..5394ee15e3 --- /dev/null +++ b/bikeshed/w3c/echidna.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import json +import re + +import requests + +from .. import messages as m +from .. import t + + +def checkEchidna(pubToken: str) -> None: + match = re.match(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", pubToken) + if not match: + m.die( + f"Publication id doesn't appear to be valid. It should be a UUID, consisting of 8-4-4-4-12 hex digits. Got:\n{pubToken}", + ) + return None + + url = f"https://labs.w3.org/echidna/api/status?id={pubToken}" + try: + response = requests.get(url, timeout=5) + except Exception as e: + m.die(f"Error retrieving the publication data.\n{e}") + return None + + text = response.text + if text.startswith("No job found with"): + m.say( + "Either the publication id is no longer valid, or the job hasn't been started yet. Try again in 10-15 seconds.", + ) + + try: + data = json.loads(text) + except Exception as e: + m.die(f"Error parsing the publication data as JSON:\n{e}") + + if "results" in data: + results = data["results"] + + if results["status"] == "started": + printProgress(results["jobs"]) + elif results["status"] == "success": + printSuccess(results["history"]) + else: + m.say(f"Some sort of failure; this error message will be improved.\n{text}") + + +def printSuccess(history: t.JSONT) -> None: + for item in history.values(): + text = item["fact"] + match = re.match("The document has been published.*", text) + if not match: + continue + match = re.search(">([^<]+)", text) + if not match: + m.say(f"Successfully published, but can't figure out where. Here's the text I tried to parse:\n{text}") + return + url = match.group(1) + break + else: + m.say( + f"Echidna claims it was published, but I couldn't find the publication entry in the history. Full history:\n{json.dumps(history, indent=2)}", + ) + return None + + m.success(f"Published to {url}") + + +def printProgress(jobs: t.JSONT) -> None: + for jobName, jobData in jobs.items(): + if jobData["status"] == "ok": + continue + elif jobData["status"] == "pending": + m.say(f"Currently pending on {printJobName(jobName)}, please wait.") + return + elif jobData["status"] == "error": + m.failure(f"Publication failed on {printJobName(jobName)}, errors are:\n{printErrors(jobData['errors'])}") + else: + m.say( + f"Unknown job status '{jobData['status']}' on {printJobName(jobName)}, so status is unknown. Here's the full job:\n{json.dumps({jobName: jobData}, indent=2)}", + ) + + +def printJobName(name: str) -> str: + return name + + +def printErrors(errors: list[str]) -> str: + return "\n".join(errors)