matrix-ops-bot/ops_bot/util/template.py

165 lines
5.2 KiB
Python
Raw Permalink Normal View History

2022-12-01 13:47:27 +00:00
# gitlab - A GitLab client and webhook receiver for maubot
# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import os
2022-12-01 13:47:27 +00:00
import os.path
2022-12-01 17:28:22 +00:00
from pathlib import Path
2022-12-01 14:20:37 +00:00
from typing import Any, Callable, Dict, List, Tuple, Union
2022-12-01 13:47:27 +00:00
2022-12-01 14:20:37 +00:00
from jinja2 import BaseLoader
from jinja2 import Environment as JinjaEnvironment
from jinja2 import Template, TemplateNotFound
2022-12-01 13:47:27 +00:00
from ops_bot import get_project_root
2022-12-01 13:47:27 +00:00
from ops_bot.util import markdown
2022-12-01 14:20:37 +00:00
2022-12-01 17:28:22 +00:00
def sync_read_file(path: Union[Path, str]) -> str:
2022-12-01 13:47:27 +00:00
with open(path) as file:
return file.read()
2022-12-01 17:28:22 +00:00
def sync_list_files(directory: Union[Path, str]) -> list[str]:
2022-12-01 13:47:27 +00:00
return os.listdir(directory)
2022-12-01 14:20:37 +00:00
2022-12-01 13:47:27 +00:00
class TemplateUtil:
@staticmethod
def bold_scope(label: str) -> str:
try:
scope, label = label.rsplit("::", 1)
return f"{scope}::<strong>{label}</strong>"
except ValueError:
return label
@staticmethod
def pluralize(val: int, unit: str) -> str:
if val == 1:
return f"{val} {unit}"
return f"{val} {unit}s"
@classmethod
def format_time(cls, seconds: Union[int, float], enable_days: bool = False) -> str:
seconds = abs(seconds)
frac_seconds = round(seconds - int(seconds), 1)
minutes, seconds = divmod(int(seconds), 60)
hours, minutes = divmod(minutes, 60)
if enable_days:
days, hours = divmod(hours, 24)
else:
days = 0
parts = []
if days > 0:
parts.append(cls.pluralize(days, "day"))
if hours > 0:
parts.append(cls.pluralize(hours, "hour"))
if minutes > 0:
parts.append(cls.pluralize(minutes, "minute"))
if seconds > 0 or len(parts) == 0:
2022-12-01 14:20:37 +00:00
parts.append(cls.pluralize(int(seconds + frac_seconds), "second"))
2022-12-01 13:47:27 +00:00
if len(parts) == 1:
return parts[0]
return ", ".join(parts[:-1]) + f" and {parts[-1]}"
@staticmethod
2022-12-01 14:20:37 +00:00
def join_human_list(
data: List[str],
*,
joiner: str = ", ",
final_joiner: str = " and ",
mutate: Callable[[str], str] = lambda val: val,
) -> str:
2022-12-01 13:47:27 +00:00
if not data:
return ""
elif len(data) == 1:
return mutate(data[0])
2022-12-01 14:20:37 +00:00
return (
joiner.join(mutate(val) for val in data[:-1])
+ final_joiner
+ mutate(data[-1])
)
2022-12-01 13:47:27 +00:00
class TemplateProxy:
_env: JinjaEnvironment
_args: Dict[str, Any]
def __init__(self, env: JinjaEnvironment, args: Dict[str, Any]) -> None:
self._env = env
self._args = args
def __getattr__(self, item: str) -> str:
try:
tpl = self._env.get_template(item)
except TemplateNotFound:
raise AttributeError(item)
return tpl.render(**self._args)
class PluginTemplateLoader(BaseLoader):
2022-12-01 17:28:22 +00:00
directory: Path
2022-12-01 13:47:27 +00:00
macros: str
2022-12-01 14:20:37 +00:00
def __init__(self, base: str, directory: str) -> None:
template_root = os.environ.get("OPS_BOT_TEMPLATE_ROOT")
if template_root is None:
template_root = str(get_project_root() / ".." / "templates")
template_root_path = Path(template_root)
base_path = template_root_path / base
self.directory = base_path / directory
self.macros = sync_read_file(base_path / "macros.html")
2022-12-01 13:47:27 +00:00
2022-12-01 14:20:37 +00:00
def get_source(
2026-03-05 16:08:31 +01:00
self, environment: Any, template: str
2022-12-01 14:20:37 +00:00
) -> Tuple[str, str, Callable[[], bool]]:
2026-03-05 16:08:31 +01:00
path = self.directory / f"{template}.html"
2022-12-01 13:47:27 +00:00
try:
tpl = sync_read_file(path)
except KeyError:
2026-03-05 16:08:31 +01:00
raise TemplateNotFound(template)
return self.macros + tpl, template, lambda: True
2022-12-01 13:47:27 +00:00
2022-12-01 14:20:37 +00:00
def list_templates(self) -> List[str]:
return [
os.path.splitext(os.path.basename(path))[0]
for path in sync_list_files(self.directory)
if path.endswith(".html")
]
2022-12-01 13:47:27 +00:00
class TemplateManager:
_env: JinjaEnvironment
_loader: PluginTemplateLoader
def __init__(self, base: str, directory: str) -> None:
self._loader = PluginTemplateLoader(base, directory)
2022-12-01 14:20:37 +00:00
self._env = JinjaEnvironment( # nosec B701
loader=self._loader,
lstrip_blocks=True,
trim_blocks=True,
autoescape=False,
extensions=["jinja2.ext.do"],
)
self._env.filters["markdown"] = lambda message: markdown.render(
message, allow_html=True
)
2022-12-01 13:47:27 +00:00
def __getitem__(self, item: str) -> Template:
return self._env.get_template(item)
def proxy(self, args: Dict[str, Any]) -> TemplateProxy:
return TemplateProxy(self._env, args)