Source code for gobstopper.templates.engine

"""
Jinja2 template engine for Gobstopper framework
"""

import json
import time
from datetime import datetime
from pathlib import Path
from typing import Union

try:
    import jinja2
    from jinja2 import Environment, FileSystemLoader, select_autoescape
    JINJA2_AVAILABLE = True
except ImportError:
    JINJA2_AVAILABLE = False
    jinja2 = None


[docs] class TemplateEngine: """Jinja2-based template engine with async support"""
[docs] def __init__(self, template_folder: Union[str, Path] = "templates", auto_reload: bool = True, cache_size: int = 400): if not JINJA2_AVAILABLE: raise ImportError("Jinja2 is required for templating. Install: uv add jinja2") self.template_folder = Path(template_folder) loader = FileSystemLoader(str(self.template_folder)) self.env = Environment( loader=loader, autoescape=select_autoescape(['html', 'xml']), auto_reload=auto_reload, cache_size=cache_size, enable_async=True ) self._add_default_filters() self._add_default_globals()
[docs] def add_search_path(self, path: Union[str, Path]): """Add an additional search path for templates (used by blueprints).""" p = str(Path(path)) try: # FileSystemLoader has .searchpath list if hasattr(self.env.loader, 'searchpath'): if p not in self.env.loader.searchpath: self.env.loader.searchpath.append(p) except Exception: pass
def _add_default_filters(self): """Add default template filters""" def tojson_filter(obj): return json.dumps(obj) def currency_filter(amount): return f"${amount:,.2f}" def relative_time_filter(timestamp): diff = time.time() - timestamp if diff < 60: return f"{int(diff)}s ago" elif diff < 3600: return f"{int(diff // 60)}m ago" elif diff < 86400: return f"{int(diff // 3600)}h ago" else: return f"{int(diff // 86400)}d ago" self.env.filters['tojson'] = tojson_filter self.env.filters['currency'] = currency_filter self.env.filters['relative_time'] = relative_time_filter def _add_default_globals(self): """Add default global variables""" self.env.globals.update({ 'now': datetime.now, 'timestamp': time.time, 'range': range, 'len': len, 'str': str, 'int': int, 'float': float, 'bool': bool, })
[docs] async def render_template_async(self, template_name: str, **context) -> str: """Render template asynchronously""" try: template = self.env.get_template(template_name) return await template.render_async(**context) except jinja2.TemplateNotFound: raise FileNotFoundError(f"Template '{template_name}' not found")
[docs] def add_filter(self, name: str, func): """Add a custom filter""" self.env.filters[name] = func
[docs] def add_global(self, name: str, func): """Add a custom global function""" self.env.globals[name] = func