mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-08-29 15:06:26 +02:00
Implemented dict renderer to resolve assets
This commit is contained in:
119
utils/dict_renderer.py
Normal file
119
utils/dict_renderer.py
Normal file
@@ -0,0 +1,119 @@
|
||||
import re
|
||||
import time
|
||||
from typing import Any, Dict, Union, List, Set
|
||||
|
||||
class DictRenderer:
|
||||
"""
|
||||
Resolves placeholders in the form << path >> within nested dictionaries,
|
||||
supporting hyphens, numeric list indexing, and quoted keys via ['key'] or ["key"].
|
||||
"""
|
||||
# Match << path >> where path contains no whitespace or closing >
|
||||
PATTERN = re.compile(r"<<\s*(?P<path>[^\s>]+)\s*>>")
|
||||
# Tokenizes a path into unquoted keys, single-quoted, double-quoted keys, or numeric indices
|
||||
TOKEN_REGEX = re.compile(
|
||||
r"(?P<key>[\w\-]+)"
|
||||
r"|\['(?P<qkey>[^']+)'\]"
|
||||
r"|\[\"(?P<dkey>[^\"]+)\"\]"
|
||||
r"|\[(?P<idx>\d+)\]"
|
||||
)
|
||||
|
||||
def __init__(self, verbose: bool = False, timeout: float = 10.0):
|
||||
self.verbose = verbose
|
||||
self.timeout = timeout
|
||||
|
||||
def render(self, data: Union[Dict[str, Any], List[Any]]) -> Union[Dict[str, Any], List[Any]]:
|
||||
start = time.monotonic()
|
||||
self.root = data
|
||||
rendered = data
|
||||
pass_num = 0
|
||||
|
||||
while True:
|
||||
pass_num += 1
|
||||
if self.verbose:
|
||||
print(f"[DictRenderer] Pass {pass_num} starting...")
|
||||
rendered, changed = self._render_pass(rendered)
|
||||
if not changed:
|
||||
if self.verbose:
|
||||
print(f"[DictRenderer] No more placeholders after pass {pass_num}.")
|
||||
break
|
||||
if time.monotonic() - start > self.timeout:
|
||||
raise TimeoutError(f"Rendering exceeded timeout of {self.timeout} seconds")
|
||||
|
||||
# After all passes, raise error on unresolved placeholders
|
||||
unresolved = self.find_unresolved(rendered)
|
||||
if unresolved:
|
||||
raise ValueError(f"Unresolved placeholders: {', '.join(sorted(unresolved))}")
|
||||
|
||||
return rendered
|
||||
|
||||
def _render_pass(self, obj: Any) -> (Any, bool):
|
||||
if isinstance(obj, dict):
|
||||
new = {}
|
||||
changed = False
|
||||
for k, v in obj.items():
|
||||
nv, ch = self._render_pass(v)
|
||||
new[k] = nv
|
||||
changed = changed or ch
|
||||
return new, changed
|
||||
if isinstance(obj, list):
|
||||
new_list = []
|
||||
changed = False
|
||||
for item in obj:
|
||||
ni, ch = self._render_pass(item)
|
||||
new_list.append(ni)
|
||||
changed = changed or ch
|
||||
return new_list, changed
|
||||
if isinstance(obj, str):
|
||||
def repl(m):
|
||||
path = m.group('path')
|
||||
val = self._lookup(path)
|
||||
if val is not None:
|
||||
if self.verbose:
|
||||
print(f"[DictRenderer] Resolving <<{path}>> -> {val}")
|
||||
return str(val)
|
||||
return m.group(0)
|
||||
new_str = self.PATTERN.sub(repl, obj)
|
||||
return new_str, new_str != obj
|
||||
return obj, False
|
||||
|
||||
def _lookup(self, path: str) -> Any:
|
||||
current = self.root
|
||||
for m in self.TOKEN_REGEX.finditer(path):
|
||||
if m.group('key') is not None:
|
||||
if isinstance(current, dict):
|
||||
current = current.get(m.group('key'))
|
||||
else:
|
||||
return None
|
||||
elif m.group('qkey') is not None:
|
||||
if isinstance(current, dict):
|
||||
current = current.get(m.group('qkey'))
|
||||
else:
|
||||
return None
|
||||
elif m.group('dkey') is not None:
|
||||
if isinstance(current, dict):
|
||||
current = current.get(m.group('dkey'))
|
||||
else:
|
||||
return None
|
||||
elif m.group('idx') is not None:
|
||||
idx = int(m.group('idx'))
|
||||
if isinstance(current, list) and 0 <= idx < len(current):
|
||||
current = current[idx]
|
||||
else:
|
||||
return None
|
||||
if current is None:
|
||||
return None
|
||||
return current
|
||||
|
||||
def find_unresolved(self, data: Any) -> Set[str]:
|
||||
"""Return all paths of unresolved << placeholders in data."""
|
||||
unresolved: Set[str] = set()
|
||||
if isinstance(data, dict):
|
||||
for v in data.values():
|
||||
unresolved |= self.find_unresolved(v)
|
||||
elif isinstance(data, list):
|
||||
for item in data:
|
||||
unresolved |= self.find_unresolved(item)
|
||||
elif isinstance(data, str):
|
||||
for m in self.PATTERN.finditer(data):
|
||||
unresolved.add(m.group('path'))
|
||||
return unresolved
|
Reference in New Issue
Block a user