Simple markdown/html report templating with loops, tables, lists, media, slots, Pydantic and SQLAlchemy.
Project description
relator
relator — библиотека шаблонов для автогенерации отчётов в Markdown и HTML.
Главная идея:
- Вы задаёте шаблон.
- По шагам добавляете данные через
template.data([name, value]). - Смотрите предпросмотр
template.print(). - Компилируете отчёт в файл
template.compile(...).
Полная документация (установка, все плейсхолдеры, интеграции, примеры): каталог docs/.
См. также: поток данных, концепции, примеры вывода Markdown, витрина в prototyping/example/00_showcase, агенты и LLM, обзор для промпта, cookbook.
Содержание
- Установка
- Быстрый старт
- Концепция шаблона
- Служебные плейсхолдеры
- Пошаговый API
- Подробные примеры
- Тяжёлые сценарии
- Media: PIL и matplotlib
- CLI
- Ошибки и отладка
- FAQ
- Рекомендации по архитектуре
- Кроссплатформенность
- Публикация и релиз
Установка
Через pip из локального проекта
cd reporting
python -m pip install -e .
Через uv из локального проекта
cd reporting
uv pip install -e .
Через pip из PyPI
python -m pip install relator
Через uv из PyPI
uv pip install relator
Дополнительно: Pydantic и SQLAlchemy
python -m pip install "relator[pydantic]" "relator[sqlalchemy]"
# или одной группой:
python -m pip install "relator[integrations]"
Быстрый старт
from reporting import Template
template = Template("template.md")
template.data(["name", [{"USER": "Ann", "SCORE": 10}, {"USER": "Bob", "SCORE": 20}]])
template.data(["names", ["Ann", "Bob", "Chris"]])
template.print()
template.compile("report.md")
Концепция шаблона
Шаблон — это обычный .md или .html файл.
Внутри него используются:
- блоки повторения
%%len=VAR ... %% - плейсхолдеры
[[...]]
Пример:
%%len=name
Строка [[CELL.ONE]] из [[CELL.COUNT]]
|[[name.KEYS]]|
|[[name.DIVIDER]]|
|[[name.VALUES]]|
%%
Служебные плейсхолдеры
Важно: служебные названия всегда в верхнем регистре.
CELL
[[CELL.INDEX]]— индекс 0..N-1[[CELL.ONE]]— индекс 1..N[[CELL.COUNT]]— длина текущего цикла
ITEM
[[ITEM]]— текущий элемент в теле%%len=VAR%%
TABLE
[[TABLE.VAR]]— таблица[[TABLE.VAR.NUMBERED]]— таблица с колонкой#[[TABLE.VAR.INDEX0]]— таблица с колонкой index0
LIST / ENUM
[[LIST.VAR]]— маркированный список[[ENUM.VAR]]— нумерованный список
MEDIA
[[MEDIA.VAR]]— универсальная вставка по типу шаблона[[MEDIA.VAR.PATH]]— путь до файла[[MEDIA.VAR.MD]]— принудительный Markdown[[MEDIA.VAR.HTML]]— принудительный HTML
Совместимость
[[VAR.TABLE]][[VAR.TABLE.NUMBERED]]
Слоты @@имя@@
Именованные вставки заполняются после подстановки [[...]] и циклов %%len%% (сырой текст/Markdown/HTML).
- В шаблоне:
@@title_suffix@@ - В коде:
template.slot("title_suffix", " — v2")илиrender(extra={"__slot__title_suffix": " — v2"})
PYDANTIC / SCHEMA
Нужен пакет: pip install relator[pydantic]. В контексте — подкласс BaseModel, инстанс или TypeAdapter.
[[PYDANTIC.VAR]]— краткое описание и таблица полей[[PYDANTIC.VAR.TABLE]]— только таблица полей[[PYDANTIC.VAR.JSON_SCHEMA]]— JSON Schema в блоке кода[[PYDANTIC.VAR.EXAMPLE]]— пример JSON-объекта по полям JSON Schema (эвристика /examplesв полях)[[SCHEMA.VAR]]— синонимPYDANTIC
SQL / ORM
[[SQL.VAR]]— значениеstrили SQLAlchemyExecutable; вывод в fencedsql. Для компиляции выражений:pip install relator[sqlalchemy]иTemplate(..., sql_dialect="sqlite")(такжеpostgresql,mysql).[[ORM.VAR.TABLE]](или.COLUMNS) — таблица колонок дляsqlalchemy.Tableили mapped-класса с__table__[[ORM.VAR.NAME]]— имя таблицы[[ORM.VAR.DDL]]—CREATE TABLEв fencedsql(диалект изsql_dialect)
Доступ к данным из Python (не для шаблона)
template.get("name")/get("name", default)template.table_keys("rows")— порядок колонок как у[[TABLE.rows]]template.pick("rows", 0, "USER")— ячейка строки
Пошаговый API
1) Создать объект
from reporting import Template
template = Template("template.md", sql_dialect="sqlite") # sql_dialect опционален
template.slot("intro", "_Черновик._")
2) Добавить данные по одной переменной
template.data(["name", [{"USER": "Ann", "SCORE": 10}]])
template.data(["names", ["Ann", "Bob"]])
3) Посмотреть результат в консоли
template.print(width=100)
4) Скомпилировать в файл
template.compile("report.md")
Функциональный шорткат
from reporting import compile_template
compile_template(
template_path="template.md",
context={"name": [{"A": 1}], "names": ["x", "y"], "__slot__intro": "…"},
output_path="report.md",
)
Ключи __slot__* в context задают слоты @@intro@@ и не используются как переменные для [[...]].
Подробные примеры
Пример A: таблица внутри цикла
Шаблон:
%%len=rows
|[[rows.KEYS]]|
|[[rows.DIVIDER]]|
|[[rows.VALUES]]|
%%
Данные:
rows = [
{"CITY": "Paris", "POP": 2148},
{"CITY": "Berlin", "POP": 3769},
]
Пример B: таблица целиком
Шаблон:
[[TABLE.rows]]
[[TABLE.rows.NUMBERED]]
Пример C: списки
Шаблон:
[[LIST.names]]
[[ENUM.names]]
Данные:
names = ["Ann", "Bob", "Chris", "Dina"]
Пример D: вложенный список
Шаблон:
%%len=names
1. [[ITEM]]
- index: [[CELL.INDEX]]
- one: [[CELL.ONE]]
%%
Тяжёлые сценарии
Ниже сценарии ближе к реальному продакшену.
Сценарий 1: еженедельный отчёт команды
Цель:
- по командам показать KPI
- дать агрегированную таблицу
- вставить график
Шаблон weekly.md:
# Еженедельный отчёт
## Команды
%%len=teams
### Команда [[ITEM]]
Индекс: [[CELL.ONE]] / [[CELL.COUNT]]
%%
## KPI
[[TABLE.kpi.NUMBERED]]
## График
[[MEDIA.kpi_chart]]
Код:
from reporting import Template
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(6, 3))
ax = fig.add_subplot(111)
ax.plot([1, 2, 3, 4], [95, 97, 96, 99], marker="o")
ax.set_title("KPI динамика")
template = Template("weekly.md", assets_dir="weekly_assets")
template.data(["teams", ["Backend", "Frontend", "QA"]])
template.data(["kpi", [
{"METRIC": "Coverage", "VALUE": "82%"},
{"METRIC": "Incidents", "VALUE": "2"},
{"METRIC": "Deploys", "VALUE": "14"},
]])
template.data(["kpi_chart", fig])
template.print()
template.compile("weekly_report.md")
Сценарий 2: HTML-отчёт для менеджмента
Шаблон management.html:
<h1>Отчёт менеджмента</h1>
<h2>Сводная таблица</h2>
[[TABLE.metrics.HTML]]
<h2>Визуализация</h2>
[[MEDIA.chart]]
Код:
from reporting import Template
from PIL import Image
logo = Image.new("RGB", (160, 60), "navy")
template = Template("management.html", assets_dir="html_assets")
template.data(["metrics", [
{"NAME": "Revenue", "VALUE": "1.2M"},
{"NAME": "Cost", "VALUE": "0.7M"},
]])
template.data(["chart", logo])
template.compile("management_report.html")
Сценарий 3: много отчётов в цикле по проектам
from pathlib import Path
from reporting import Template
projects = {
"alpha": {"issues": 14, "coverage": "79%"},
"beta": {"issues": 4, "coverage": "88%"},
"gamma": {"issues": 0, "coverage": "93%"},
}
for project_name, data in projects.items():
t = Template("project_template.md", assets_dir=Path("assets") / project_name)
t.data(["project_name", project_name])
t.data(["metrics", [{"KEY": "issues", "VALUE": data["issues"]}, {"KEY": "coverage", "VALUE": data["coverage"]}]])
t.compile(Path("reports") / f"{project_name}.md")
Сценарий 4: пакетная генерация + предпросмотр только при ошибках
from reporting import Template, TemplateError
def render_safe(template_path: str, out_path: str, context: dict):
t = Template(template_path)
for k, v in context.items():
t.data([k, v])
try:
t.compile(out_path)
except TemplateError:
t.print()
raise
Media: PIL и matplotlib
PIL.Image
from PIL import Image
from reporting import Template
img = Image.new("RGB", (120, 40), "steelblue")
t = Template("template.md", assets_dir="assets")
t.data(["logo", img])
t.compile("report.md")
matplotlib.figure.Figure
import matplotlib.pyplot as plt
from reporting import Template
fig = plt.figure()
ax = fig.add_subplot(111)
ax.bar(["A", "B", "C"], [3, 7, 5])
t = Template("template.md")
t.data(["chart", fig])
t.compile("report.md")
Управление вставкой
[[MEDIA.chart]] # авто (md/html)
[[MEDIA.chart.PATH]] # только путь
[[MEDIA.chart.MD]] # строго markdown
[[MEDIA.chart.HTML]] # строго html
CLI
Манифест шаблона (JSON)
relator --template template.md --inspect
Вывод — список ожидаемых ключей контекста, слотов @@...@@ и сырых плейсхолдеров (удобно для промпта агента и CI).
Компиляция в файл
relator --template template.md --context context.json --output report.md
Только печать
relator --template template.md --context context.json --print
Пример context.json со слотами
Ключи __slot__имя задают слоты @@имя@@ (не попадают в контекст [[...]]):
{
"rows": [{"A": 1}],
"__slot__note": "_Черновик._"
}
Пример context.json (только данные)
{
"name": [
{"USER": "Ann", "SCORE": 10},
{"USER": "Bob", "SCORE": 20}
],
"names": ["Ann", "Bob", "Chris"]
}
TemplateError: Unknown placeholder root ...
Причина:
- в шаблоне используется переменная, которую не добавили через
data.
Решение:
- проверить имя переменной и регистр.
CELL.* is available only inside %%len=VAR%%
Причина:
CELLиспользован вне цикла.
Решение:
- перенести плейсхолдер в тело
%%len=...%%.
VAR.VALUES requires %%len=VAR%% context
Причина:
[[VAR.VALUES]]использован вне своего цикла.
Решение:
- использовать
[[TABLE.VAR]]или обернуть в%%len=VAR%%.
FAQ
Можно ли использовать lowercase имена переменных?
Да. Например name, rows, items.
Почему служебные плейсхолдеры uppercase?
Чтобы отделять пользовательские данные от системных токенов.
Можно ли использовать библиотеку без Rich?
Да, но метод print() требует rich.
Можно ли рендерить только в строку без файла?
Да, через template.render().
Поддерживается ли Windows?
Да, библиотека использует pathlib и UTF-8.
Рекомендации по архитектуре
- Храните шаблоны в
templates/. - Храните generated отчёты в
reports/. - Для медиа используйте отдельную папку
assets/. - Не смешивайте бизнес-логику и шаблонную логику.
- Добавьте golden-тесты для ключевых шаблонов.
Пример структуры:
project/
templates/
weekly.md
management.html
reports/
assets/
scripts/
build_reports.py
Кроссплатформенность
- операции с путями через
pathlib.Path - чтение/запись через UTF-8
- CI-матрица: Linux, macOS, Windows
- тестовая матрица Python: 3.9–3.13
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file relator-1.1.0.tar.gz.
File metadata
- Download URL: relator-1.1.0.tar.gz
- Upload date:
- Size: 20.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
386248c7801486c08d82a7d9d4593361947c32cecba4697e8f107228c9129869
|
|
| MD5 |
bbc83b24eb60d9b19ab746f115d96df2
|
|
| BLAKE2b-256 |
df1b7b8f4f283ed2d07e40047976266d90f5b5cb1fcb9a9e930cf5ca7e4370cd
|
File details
Details for the file relator-1.1.0-py3-none-any.whl.
File metadata
- Download URL: relator-1.1.0-py3-none-any.whl
- Upload date:
- Size: 21.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c239c343d6e2aa7327fd91428252137033745e1764c29b5b2669ef9d8976b9a9
|
|
| MD5 |
89cadefa98ba10fab6fa012f702ca727
|
|
| BLAKE2b-256 |
2abe2fff8675295dd77f23e4a9ec63ec25378c4a80339c93b5af7faa4da19245
|