Extensions: add HTML generation to the "server-generate" command

- Optionally generate a simple HTML page (using --html).
- Links include repository, blender_versnon_* & platform information,
  so the information can be used by blender when links are dropped
  into Blender.
- Support using a template as the default HTML is primitive.
This commit is contained in:
Campbell Barton
2024-06-22 23:48:00 +10:00
parent fc3e0507cc
commit cf73897690
2 changed files with 185 additions and 1 deletions

View File

@@ -42,7 +42,7 @@ def size_as_fmt_string(num: float, *, precision: int = 1) -> str:
if abs(num) < 1024.0:
return "{:3.{:d}f}{:s}".format(num, precision, unit)
num /= 1024.0
unit = "yb"
unit = "YB"
return "{:.{:d}f}{:s}".format(num, precision, unit)

View File

@@ -105,6 +105,22 @@ CHUNK_SIZE_DEFAULT = 1 << 14
# Used for project tag-line & permissions values.
TERSE_DESCRIPTION_MAX_LENGTH = 64
# Default HTML for `server-generate`.
# Intentionally very basic, users may define their own `--html-template`.
HTML_TEMPLATE = '''\
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Blender Extensions</title>
</head>
<body>
<p>Blender Extension Listing:</p>
${body}
<center><p>Built ${date}</p></center>
</body>
</html>
'''
# Standard out may be communicating with a parent process,
# arbitrary prints are NOT acceptable.
@@ -188,6 +204,16 @@ def force_exit_ok_enable() -> None:
# -----------------------------------------------------------------------------
# Generic Functions
def size_as_fmt_string(num: float, *, precision: int = 1) -> str:
for unit in ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB"):
if abs(num) < 1024.0:
return "{:3.{:d}f}{:s}".format(num, precision, unit)
num /= 1024.0
unit = "YB"
return "{:.{:d}f}{:s}".format(num, precision, unit)
def read_with_timeout(fh: IO[bytes], size: int, *, timeout_in_seconds: float) -> Optional[bytes]:
# TODO: implement timeout (TimeoutError).
_ = timeout_in_seconds
@@ -2287,6 +2313,37 @@ def generic_arg_build_split_platforms(subparse: argparse.ArgumentParser) -> None
)
# -----------------------------------------------------------------------------
# Argument Handlers ("server-generate" command)
def generic_arg_server_generate_html(subparse: argparse.ArgumentParser) -> None:
subparse.add_argument(
"--html",
dest="html",
action="store_true",
default=False,
help=(
"Create a HTML file (``index.html``) as well as the repository JSON "
"to support browsing extensions online with static-hosting."
),
)
def generic_arg_server_generate_html_template(subparse: argparse.ArgumentParser) -> None:
subparse.add_argument(
"--html-template",
dest="html_template",
default="",
help=(
"Optionally override the default HTML template with your own.\n"
"\n"
"The following keys are supported.\n"
"- ``${body}`` is replaced the extensions contents.\n"
"- ``${date}`` is replaced the creation date.\n"
),
)
# -----------------------------------------------------------------------------
# Generate Repository
@@ -2560,11 +2617,122 @@ class subcmd_server:
def __new__(cls) -> Any:
raise RuntimeError("{:s} should not be instantiated".format(cls))
@staticmethod
def _generate_html(
msg_fn: MessageFn,
*,
repo_dir: str,
repo_data: List[Dict[str, Any]],
html_template_filepath: str,
) -> bool:
import html
import datetime
from string import Template
import urllib
import urllib.parse
filepath_repo_html = os.path.join(repo_dir, "index.html")
fh = io.StringIO()
fh.write("<table>\n")
fh.write(" <tr>\n")
fh.write(" <th>ID</th>\n")
fh.write(" <th>Name</th>\n")
fh.write(" <th>Description</th>\n")
fh.write(" <th>Blender Versions</th>\n")
fh.write(" <th>Platforms</th>\n")
fh.write(" <th>Size</th>\n")
fh.write(" </tr>\n")
for manifest_dict in sorted(repo_data, key=lambda manifest: (manifest["id"], manifest["version"])):
fh.write(" <tr>\n")
platforms = [platform for platform in manifest_dict.get("platforms", "").split(",") if platform]
# Parse the URL and add parameters use for drag & drop.
parsed_url = urllib.parse.urlparse(manifest_dict["archive_url"])
# We could support existing values, currently always empty.
# `query = dict(urllib.parse.parse_qsl(parsed_url.query))`
query = {"repository": "/index.json"}
if (value := manifest_dict.get("blender_version_min", "")):
query["blender_version_min"] = value
if (value := manifest_dict.get("blender_version_max", "")):
query["blender_version_max"] = value
if platforms:
query["platforms"] = ",".join(platforms)
del value
id_and_link = "<a href=\"{:s}\">{:s}</a>".format(
urllib.parse.urlunparse((
parsed_url.scheme,
parsed_url.netloc,
parsed_url.path,
parsed_url.params,
urllib.parse.urlencode(query, doseq=True) if query else None,
parsed_url.fragment,
)),
html.escape("{:s}-{:s}".format(manifest_dict["id"], manifest_dict["version"])),
)
# Write the table data.
fh.write(" <td><tt>{:s}</tt></td>\n".format(id_and_link))
fh.write(" <td>{:s}</td>\n".format(html.escape(manifest_dict["name"])))
fh.write(" <td>{:s}</td>\n".format(html.escape(manifest_dict["tagline"] or "<NA>")))
blender_version_min = manifest_dict.get("blender_version_min", "")
blender_version_max = manifest_dict.get("blender_version_max", "")
if blender_version_min or blender_version_max:
blender_version_str = "{:s} - {:s}".format(
blender_version_min or "~",
blender_version_max or "~",
)
else:
blender_version_str = "all"
fh.write(" <td>{:s}</td>\n".format(html.escape(blender_version_str)))
fh.write(" <td>{:s}</td>\n".format(html.escape(", ".join(platforms) if platforms else "all")))
fh.write(" <td>{:s}</td>\n".format(html.escape(size_as_fmt_string(manifest_dict["archive_size"]))))
fh.write(" </tr>\n")
fh.write("</table>\n")
body = fh.getvalue()
del fh
html_template_text = ""
if html_template_filepath:
try:
with open(html_template_filepath, "r", encoding="utf-8") as fh_html:
html_template_text = fh_html.read()
except Exception as ex:
message_error(msg_fn, "HTML template failed to read: {:s}".format(str(ex)))
return False
else:
html_template_text = HTML_TEMPLATE
template = Template(html_template_text)
del html_template_text
try:
result = template.substitute(
body=body,
date=html.escape(datetime.datetime.now(tz=datetime.timezone.utc).strftime("%Y-%m-%d, %H:%M")),
)
except KeyError as ex:
message_error(msg_fn, "HTML template error: {:s}".format(str(ex)))
return False
del template
with open(filepath_repo_html, "w", encoding="utf-8") as fh_html:
fh_html.write(result)
return True
@staticmethod
def generate(
msg_fn: MessageFn,
*,
repo_dir: str,
html: bool,
html_template: str,
) -> bool:
if url_has_known_prefix(repo_dir):
@@ -2644,6 +2812,16 @@ class subcmd_server:
continue
if (error := pkg_manifest_detect_duplicates(pkg_idname, pkg_items)) is not None:
message_warn(msg_fn, "archive found with duplicates for id {:s}: {:s}".format(pkg_idname, error))
if html:
if not subcmd_server._generate_html(
msg_fn,
repo_dir=repo_dir,
repo_data=repo_data,
html_template_filepath=html_template,
):
return False
del repo_data_idname_map
filepath_repo_json = os.path.join(repo_dir, PKG_REPO_LIST_FILENAME)
@@ -3635,6 +3813,8 @@ def unregister():
if not subcmd_server.generate(
msg_fn_no_done,
repo_dir=repo_dir,
html=True,
html_template="",
):
# Error running command.
return False
@@ -3689,6 +3869,8 @@ def argparse_create_server_generate(
)
generic_arg_repo_dir(subparse)
generic_arg_server_generate_html(subparse)
generic_arg_server_generate_html_template(subparse)
if args_internal:
generic_arg_output_type(subparse)
@@ -3696,6 +3878,8 @@ def argparse_create_server_generate(
func=lambda args: subcmd_server.generate(
msg_fn_from_args(args),
repo_dir=args.repo_dir,
html=args.html,
html_template=args.html_template,
),
)