Source code for rs_files_templates.renderer

"""Rendering engine for file-template models."""

from __future__ import annotations

import json
import zipfile
from collections.abc import Iterable, Mapping
from dataclasses import dataclass
from functools import partial
from importlib.resources import files
from io import BytesIO
from pathlib import Path
from typing import Any

from jinja2 import Environment, FileSystemLoader, StrictUndefined

from .models import FileTemplateModel
from .validation import validate_rendered


[docs] @dataclass(frozen=True, slots=True) class GeneratedFile: """Result of rendering one file model. Parameters ---------- model Concrete model class name used for rendering. path Generated file path. validated Whether an output schema was checked successfully. """ model: str path: Path validated: bool
def _environment() -> Environment: """Create the package Jinja environment.""" template_dir = files("rs_files_templates.templates") environment = Environment( loader=FileSystemLoader(str(template_dir)), undefined=StrictUndefined, keep_trailing_newline=True, autoescape=False, trim_blocks=True, lstrip_blocks=True, ) # Standalone files do not need Jinja's HTML-safe escaping of <, >, and &. environment.filters["tojson"] = partial(json.dumps, ensure_ascii=False) return environment
[docs] def render_text(model: FileTemplateModel) -> str: """Render one model without writing it to disk. Parameters ---------- model Validated file-template model. Returns ------- str Rendered content. """ return render_template(model.template_name, model.template_data())
[docs] def render_template(template_name: str, data: Mapping[str, Any]) -> str: """Render one packaged template by name. Used by ``zip``-typed models to compose several packaged templates into one archive without a dedicated model per archive member. """ return _environment().get_template(template_name).render(model=data)
def _build_zip(entries: Mapping[str, str]) -> bytes: """Pack rendered file contents into an in-memory zip archive.""" buffer = BytesIO() with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive: for name, content in entries.items(): archive.writestr(name, content) return buffer.getvalue()
[docs] def render_file( model: FileTemplateModel, output_dir: str | Path, *, validate_schema: bool = False, ) -> GeneratedFile: """Render one model to its default output path. Parameters ---------- model Validated file-template model. output_dir Directory receiving the generated file. validate_schema Validate the rendered file when the model declares a schema. Returns ------- GeneratedFile Generation result. """ root = Path(output_dir) target = root / model.output_name target.parent.mkdir(parents=True, exist_ok=True) if model.media_type == "zip": target.write_bytes(_build_zip(model.archive_entries())) else: target.write_text(render_text(model), encoding="utf-8") validated = False if validate_schema and model.schema_url: validate_rendered(target, model.schema_url, model.media_type) validated = True return GeneratedFile(type(model).__name__, target, validated)
[docs] def render_many( models: Iterable[FileTemplateModel], output_dir: str | Path, *, validate_schemas: bool = False, ) -> list[GeneratedFile]: """Render several independent file models into one directory.""" return [render_file(model, output_dir, validate_schema=validate_schemas) for model in models]