from __future__ import annotations import re from collections.abc import Mapping from typing import Any _DOLLAR_FIELD_PATTERN = re.compile(r"(? str: key = raw.strip() if key.startswith("fields."): key = key.removeprefix("fields.") elif key.startswith("local."): key = "local::" + key.removeprefix("local.") elif key.startswith("global."): key = "global::" + key.removeprefix("global.") if key.startswith("local::") or key.startswith("global::"): return key if key.startswith("local:"): return "local::" + key.removeprefix("local:") if key.startswith("global:"): return "global::" + key.removeprefix("global:") return key def render_template( template: str, values: Mapping[str, Any], *, keep_missing: bool = True, ) -> str: def replace(match: re.Match[str]) -> str: key = normalize_template_key(match.group(1)) if key in values: value = values[key] return "" if value is None else str(value) return match.group(0) if keep_missing else "" rendered = _DOLLAR_FIELD_PATTERN.sub(replace, template) rendered = _BRACE_FIELD_PATTERN.sub(replace, rendered) return rendered.replace(r"\${", "${").replace(r"\}", "}") def find_unresolved_placeholders(text: str | None) -> set[str]: if not text: return set() return { normalize_template_key(match.group(1)) for pattern in (_DOLLAR_FIELD_PATTERN, _BRACE_FIELD_PATTERN) for match in pattern.finditer(text) }