Metadata-Version: 2.4
Name: vcti-template
Version: 2.0.1
Summary: Jinja2-based template rendering engine with streaming support
Author: Visual Collaboration Technologies Inc.
License-Expression: LicenseRef-Proprietary
Project-URL: Repository, https://github.com/vcollab/vcti-python-template
Project-URL: Changelog, https://github.com/vcollab/vcti-python-template/blob/main/CHANGELOG.md
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: <3.15,>=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: jinja2>=3.1
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: pytest-asyncio; extra == "test"
Provides-Extra: lint
Requires-Dist: ruff; extra == "lint"
Provides-Extra: typecheck
Requires-Dist: mypy; extra == "typecheck"
Dynamic: license-file

# Template

Jinja2-based template rendering engine with streaming support for Python.

## Overview

VCollab applications render data into various output formats — HTML reports,
chart visualizations, CSV exports, styled tables. The rendering logic is
always the same: load a template, pass data, produce output.

`vcti-template` provides a lightweight engine for this pattern. Templates come
from three sources — a named **registry** of strings, the **filesystem**, or an
**inline** string — all behind one Jinja2 loader, so they share a compiled
cache and can `{% extends %}` or `{% include %}` each other in any direction.
On top of that: streaming for large datasets, a typed exception hierarchy, an
autoescape default that configuration cannot weaken, and an optional sandbox.

## Installation

```bash
pip install vcti-template

# Specific version
pip install vcti-template==2.0.1
```

## Quick Start

### Registry + Manager

```python
from vcti.template import TemplateManager, TemplateRegistry

registry = TemplateRegistry()
registry.register("greeting", "Hello, {{ name }}!")
registry.register("row", "<tr><td>{{ value }}</td></tr>")

tm = TemplateManager(registry=registry)
tm.render("greeting", name="World")  # "Hello, World!"
```

The manager holds a live reference, so templates registered later are picked up
on the next render.

### Filesystem and Inline

```python
tm = TemplateManager(template_dirs=["./templates"])
tm.render("report.html", title="Q1 Report", data=summary)

tm.render_string("{{ x }} + {{ y }} = {{ x + y }}", x=2, y=3)  # "2 + 3 = 5"
```

Inline templates are cached by source string, so rendering one in a loop does
not recompile it.

### Inheritance Across Sources

Either source can extend or include the other; registry entries shadow files
of the same name.

```python
registry.register("layout", "<html><body>{% block body %}{% endblock %}</body></html>")
tm = TemplateManager(registry=registry, template_dirs=["./templates"])

# ./templates/invoice.html: {% extends 'layout' %}{% block body %}…{% endblock %}
tm.render("invoice.html", n=1042)
```

### Streaming for Large Data

```python
registry.register("header", "<table><tr><th>Name</th></tr>")
registry.register("footer", "</table>")

chunks = ({"name": row.name} for row in large_dataset.iter_rows())

for fragment in tm.render_streaming(
    "row", chunks, header_template="header", footer_template="footer"
):
    f.write(fragment)
```

Templates resolve when `render_streaming()` is called — including everything
they `include` or `extend` — so a missing one raises there, not partway
through after half a file is written. Runtime targets like
`{% include some_var %}` are the exception and still fail during rendering.

### Strict Mode

```python
from vcti.template import TemplateRenderError

tm = TemplateManager(strict=True)

try:
    tm.render_string("Hello, {{ name }}!")  # no name provided
except TemplateRenderError as exc:
    print(exc)  # 'name' is undefined
```

Worth defaulting on for machine-consumed output, where a silently missing value
is worse than a loud failure.

### Custom Filters and Globals

```python
tm = TemplateManager(
    filters={"shout": lambda s: s.upper() + "!!!"},
    template_globals={"app_name": "MyApp"},
)
tm.render_string("{{ app_name }}: {{ msg|shout }}", msg="hello")  # "MyApp: HELLO!!!"
```

Custom names take precedence over Jinja2's builtins.

### Async Rendering (FastAPI, asyncio)

```python
tm = TemplateManager(registry=registry)

result = await tm.arender("greeting", name="World")

# Not awaited — returns an async iterator, and accepts async iterables
# so chunks can come straight off a cursor.
async for fragment in tm.arender_streaming("row", fetch_rows()):
    await response.write(fragment)
```

Filters and globals may be coroutines here; they are awaited automatically.

## Error Handling

All template-specific errors inherit from `TemplateError`:

| Exception | When |
|-----------|------|
| `TemplateNotFoundError` | Name not in registry or filesystem, or a missing `extends`/`include` target |
| `TemplateSyntaxError` | Invalid Jinja2 syntax |
| `TemplateRenderError` | Render failure (e.g., undefined var in strict mode) |
| `TemplateSecurityError` | Sandbox violation (subclass of `TemplateRenderError`) |

Every Jinja2 failure the engine encounters is mapped onto this hierarchy. An
ordinary exception raised by *your own* filter or global propagates unchanged;
one that is itself a `jinja2.TemplateError` cannot be told apart from the
engine's own and arrives as `TemplateRenderError`.

## API Summary

### TemplateRegistry

| Method | Description |
|--------|-------------|
| `register(name, template, *, replace=False)` | Register a template string |
| `get(name)` | Retrieve a template string |
| `has(name)` | Check if name is registered |
| `remove(name)` | Remove a template |
| `names()` | List all registered names |
| `templates` | Immutable view of all templates |

### TemplateManager

| Method | Description |
|--------|-------------|
| `render(name, **context)` | Render a named template |
| `render_string(template, **context)` | Render an inline template string |
| `render_streaming(name, chunks, ...)` | Yield rendered fragments per chunk |
| `arender(name, **context)` | Async render a named template |
| `arender_string(template, **context)` | Async render an inline template string |
| `arender_streaming(name, chunks, ...)` | Async yield rendered fragments |

| Constructor Option | Default | Description |
|-------------------|---------|-------------|
| `registry` | `None` | Template registry instance (held live) |
| `template_dirs` | `None` | Filesystem directories to search |
| `extra_autoescape_extensions` | `None` | Extensions to autoescape *in addition to* the defaults |
| `strict` | `False` | Raise on undefined variables |
| `sandboxed` | `False` | Render in Jinja2's sandbox |
| `auto_reload` | `True` | Re-check templates for changes on every lookup |
| `filters` | `None` | Custom Jinja2 filters |
| `template_globals` | `None` | Variables available in all templates |

Resolution order for `render()`: registry → filesystem → error.

Set `auto_reload=False` in production, where templates do not change while the
process runs. It removes a `stat()` syscall per render, which is a substantial
saving for filesystem templates and close to nothing for registry-only ones.
The trade is that template edits, `replace=True`, and `remove()` stop taking
effect for a template already in Jinja2's template cache. That cache is a
bounded LRU (400 templates by default), so one evicted under pressure is
recompiled from current source and the change appears after all — treat this
as a performance option, not a guarantee that templates are frozen.

## Security

**Autoescaping is on by default and cannot be configuration-disabled.**
`html`, `htm`, `xhtml` and `xml` are always escaped, as are inline strings and
extensionless registry names; `extra_autoescape_extensions` only ever *widens*
that set.

**Never render untrusted template code.** `render_string()` compiles and
executes arbitrary Jinja2 — treat it like `eval()`. Untrusted input belongs in
the context, never in the template source:

```python
registry.register("report", "Hello, {{ user_input }}")

tm.render("report", user_input=request.form["name"])  # SAFE — a context value
tm.render_string(request.form["template"])            # DANGEROUS — run as code
```

`sandboxed=True` blocks private-attribute access and unsafe calls if you must
render templates you did not author. It narrows the attack surface but is not
a guarantee — see
[SECURITY.md](https://github.com/vcollab/vcti-python-template/blob/main/SECURITY.md).

## Thread Safety

`TemplateManager` rendering is thread-safe, and one manager can back many
concurrent coroutines. `TemplateRegistry` *mutation* is not — guard it with a
`threading.Lock` if you register from multiple threads.

## Dependencies

- `jinja2>=3.1` — template engine

## Documentation

| If you want to… | Read |
|---|---|
| Get started using the package | Quick Start above |
| See practical, real-world usage | [docs/patterns.md](https://github.com/vcollab/vcti-python-template/blob/main/docs/patterns.md) |
| Understand the architecture and design decisions | [docs/design.md](https://github.com/vcollab/vcti-python-template/blob/main/docs/design.md) |
| Navigate and understand the source | [docs/source-guide.md](https://github.com/vcollab/vcti-python-template/blob/main/docs/source-guide.md) |
| Run working examples | [examples/](https://github.com/vcollab/vcti-python-template/tree/main/examples) |
