Metadata-Version: 2.4
Name: scrippy-template
Version: 2.0.0
Summary: "Gestion simplifiée des fichiers modèles"
Home-page: https://codeberg.org/scrippy/scrippy-template
Author: Michael Costa, Florent Chevalier
Author-email: michael.costa@mcos.nc, florent.chevalier.nc@gmail.com
License: MIT
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Environment :: Console
Classifier: Development Status :: 5 - Production/Stable
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: jinja2>=2.10.3
Provides-Extra: dev
Requires-Dist: twine; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: wheel>=0.37.0; extra == "dev"
Requires-Dist: flake8>=4.0.1; extra == "dev"
Requires-Dist: flake8-breakpoint>=1.1.0; extra == "dev"
Requires-Dist: flake8-builtins>=1.5.3; extra == "dev"
Requires-Dist: flake8-print>=4.0.0; extra == "dev"
Requires-Dist: flake8-return>=1.1.3; extra == "dev"
Requires-Dist: pep8-naming>=0.8.2; extra == "dev"
Requires-Dist: pytest>=6.2.2; extra == "dev"
Requires-Dist: pytest-cov>=3.0.0; extra == "dev"
Requires-Dist: bump2version>=1.0.1; extra == "dev"
Requires-Dist: setuptools>=60.9.2; extra == "dev"
Requires-Dist: pypirc-chappers>=1.0.8; extra == "dev"
Requires-Dist: pylint>=2.12.2; extra == "dev"
Dynamic: license-file


![Build Status](https://drone-ext.mcos.nc/api/badges/scrippy/scrippy-template/status.svg) ![License](https://img.shields.io/static/v1?label=license&color=orange&message=MIT) ![Language](https://img.shields.io/static/v1?label=language&color=informational&message=Python)


![Scrippy, my scrangourou friend](./scrippy-template.png "Scrippy, my scrangourou friend")

# `scrippy_template`

Simplified management of template files for the [`Scrippy`](https://codeberg.org/scrippy) framework.

## Prerequisites

- Python >= 3.11

### Python modules

#### List of required modules

The modules listed below will be automatically installed.

- jinja2

## Installation

### Manual

```bash
git clone https://codeberg.org/scrippy/scrippy-template.git
cd scrippy-template
python -m pip install -r requirements.txt
make install
```

### With `pip`

```bash
pip install scrippy-template
```

### Usage

This module allows document generation from template files using the *[Jinja2](https://jinja.palletsprojects.com/)* rendering engine.

#### Loading and rendering a template file

The `template.Renderer` object is instantiated with:
- `base_path`: the directory in which template files are searched for,
- `template_filename`: the name of the template file, relative to `base_path`.

By convention, the template files used by the _Scrippy_ framework are stored in the directory defined by the `env.templatedir` configuration parameter (see the configuration of the [_Scrippy_](https://codeberg.org/scrippy) framework in the relevant [documentation](https://codeberg.org/scrippy/scrippy-core)).

The template file is checked as soon as it is loaded: a missing or unreadable template file, or a template file with a syntax error, raises a `ScrippyTemplateError` at instantiation.

The `Renderer.render(params)` method returns the rendering of the template file. **The variables MUST be supplied to the template file as a dictionary named `params`**, and are accessed from the template file as `{{params.<variable>}}`.

The template file is compiled at each rendering: a change made to the template file after it has been loaded is taken into account.

The `Renderer.load(base_path, template_filename)` method allows the same `Renderer` object to load another template file, possibly from another directory. The previously loaded template file is replaced.

```python
from scrippy_template import template

renderer = template.Renderer("/var/scrippy/templates", "dead_parrot.j2")
print(renderer.render({"user": "Harry Fink"}))

renderer.load("/var/scrippy/other_templates", "cheese_shop.j2")
print(renderer.render({"cheese": "Red Leicester"}))
```

#### Undefined variables

Any variable used by the template file **MUST** be present in the `params` dictionary: an undefined variable raises a `ScrippyTemplateError` at rendering.

Optional variables must be explicitly handled in the template file, either with the `is defined` test or with the `default` filter:

```txt
Hello {{params.user}}{% if params.title is defined %} ({{params.title}}){% endif %}
Sent by {{params.sender | default("Scrippy")}}
```

#### HTML/XML escaping

Variables are automatically escaped only in template files whose name ends with one of the following extensions: `.html`, `.htm`, `.xml`, `.html.j2`, `.htm.j2`, `.xml.j2`.

All other template files (`.j2`, `.txt`...) are rendered without any escaping.

With the template file `report.html.j2`:

```txt
<p>{{params.msg}}</p>
```

and `params = {"msg": "<b>Spam & eggs</b>"}`, the rendering is:

```txt
<p>&lt;b&gt;Spam &amp; eggs&lt;/b&gt;</p>
```

#### Jinja2 environment

The _Jinja2_ environment is accessible through the `env` attribute of the `template.Renderer` object. It is created at instantiation and kept when another template file is loaded with `Renderer.load()`.

Changes made to the environment (filters, globals, options...) are taken into account at the next rendering:

```python
renderer = template.Renderer("/var/scrippy/templates", "shout.j2")  # {{params.msg | shout}}
renderer.env.filters["shout"] = str.upper
print(renderer.render({"msg": "Spam, spam, spam"}))  # SPAM, SPAM, SPAM
```

#### Errors

All errors raise a `scrippy_template.ScrippyTemplateError`:

| Error | Raised by | Message |
|---|---|---|
| Missing template file | `Renderer()`, `Renderer.load()`, `Renderer.render()` | `Template not found: <base_path>/<template_filename>` |
| Syntax error, unknown filter | `Renderer()`, `Renderer.load()`, `Renderer.render()` | `Template syntax error: <base_path>/<template_filename> (line <n>): <message>` |
| Undefined variable | `Renderer.render()` | `Undefined variable: <message>` |
| Any other error | `Renderer()`, `Renderer.load()`, `Renderer.render()` | `Unexpected error: [<exception class>] <message>` |

```python
from scrippy_template import template, ScrippyTemplateError

try:
  renderer = template.Renderer("/var/scrippy/templates", "dead_parrot.j2")
  print(renderer.render(params))
except ScrippyTemplateError as err:
  print(err)
```

#### Simple template file

With the following template file named `dead_parrot.j2` located in the directory `/var/scrippy/templates`:

```txt
Hello {{params.user}}

You received this email because you are a member of the functional administrators of {{params.app}}.

The {{params.script}} script execution on {{params.date}} ended with the following error code:
- {{params.error.code}}: {{params.error.msg}}

--
Regards.
{{params.sender}}
```

The template file can be used as follows:

```python
import datetime
from scrippy_template import template

params = {"user": "Harry Fink",
          "app": "Flying Circus",
          "script": "dead_parrot.py",
          "date": datetime.datetime.now().strftime("%Y/%m/%d"),
          "error": {"code": 42,
                    "msg": "It's not pinin'! It's passed on! This parrot is no more!"},
          "sender": "Luiggi Vercotti"}

base_path = "/var/scrippy/templates"
template_file = "dead_parrot.j2"
renderer = template.Renderer(base_path, template_file)

print(renderer.render(params))
```

The rendering will be:

```txt
Hello Harry Fink

You received this email because you are a member of the functional administrators of Flying Circus.

The dead_parrot.py script execution on 2019/09/15 ended with the following error code:
- 42: It's not pinin'! It's passed on! This parrot is no more!

--
Regards.
Luiggi Vercotti
```

#### Template file with control structures

```python
params = {"user": "Harry Fink",
          "app": "Flying Circus",
          "script": "dead_parrot.py",
          "date": datetime.datetime.now().strftime("%Y/%m/%d"),
          "num_errors": 42,
          "sender": "Luiggi Vercotti"}
```

```txt
Hello {{params.user}}

You received this email because you are a member of the functional administrators of {{params.app}}.

The {{params.script}} script execution on {{params.date}} ended
{%- if params.num_errors == 0 %} without error.
{%- else %} with {{params.num_errors}} error(s).
{%- endif %}

--
Regards.
{{params.sender}}
```

The rendering will be:

```txt
Hello Harry Fink

You received this email because you are a member of the functional administrators of Flying Circus.

The dead_parrot.py script execution on 2019/09/15 ended with 42 error(s).

--
Regards.
Luiggi Vercotti
```

#### Template file with loop

```python
params = {"user": "Harry Fink",
          "app": "Flying Circus",
          "script": "dead_parrot.py",
          "date": datetime.datetime.now().strftime("%Y/%m/%d"),
          "errors": [{"code": 2, "msg": "It's not pinin'! It's passed on! This parrot is no more!"},
                     {"code": 3, "msg": "Ohh! The cat's eaten it."}],
          "sender": "Luiggi Vercotti"}
```

```txt
Hello {{params.user}}

You received this email because you are a member of the functional administrators of {{params.app}}.

The {{params.script}} script execution on {{params.date}} ended with the following errors:
{% for error in params.errors -%}
- {{error.code}}: {{error.msg}}
{% endfor %}
--
Regards.
{{params.sender}}
```

The rendering will be:

```txt
Hello Harry Fink

You received this email because you are a member of the functional administrators of Flying Circus.

The dead_parrot.py script execution on 2019/09/15 ended with the following errors:
- 2: It's not pinin'! It's passed on! This parrot is no more!
- 3: Ohh! The cat's eaten it.

--
Regards.
Luiggi Vercotti
```
