feat: filename template expansion with {date} placeholder
- Add expand_filename_template() to yaml_config.py
- Supports {author}, {from}, {to}, {date}, {ext} placeholders
- {date} formats as DD_MM_YYYY (e.g. 31_03_2026)
- Spaces in author replaced with underscores for safe filenames
- Unknown placeholders left as-is
- Add comprehensive CONFIG.md documentation covering YAML setup, migration, and template syntax
This commit is contained in:
188
docs/CONFIG.md
Normal file
188
docs/CONFIG.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# Настройка redmine-reporter
|
||||||
|
|
||||||
|
## Источники конфигурации
|
||||||
|
|
||||||
|
Приоритет, от высшего к низшему:
|
||||||
|
|
||||||
|
```
|
||||||
|
CLI-флаги > переменные окружения > .env > YAML > кодовые дефолты
|
||||||
|
```
|
||||||
|
|
||||||
|
Если значение не задано на верхнем уровне, берётся уровень ниже. `.env` и YAML
|
||||||
|
работают одновременно — можно оставить оба, можно удалить `.env` после миграции.
|
||||||
|
|
||||||
|
## YAML-конфиг
|
||||||
|
|
||||||
|
Основной файл: `~/.config/redmine-reporter/config.yml`.
|
||||||
|
|
||||||
|
Создаётся с правами `0600` (владелец: чтение/запись, остальные: доступ запрещён).
|
||||||
|
Директория `~/.config/redmine-reporter/` — с правами `0700`.
|
||||||
|
|
||||||
|
Если права файла шире `0600`, при запуске выводится предупреждение.
|
||||||
|
|
||||||
|
### Структура
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
redmine:
|
||||||
|
url: https://red.eltex.loc # URL инстанса Redmine
|
||||||
|
api_key: ${REDMINE_API_KEY} # API-ключ (или plaintext)
|
||||||
|
author: "Кокос А.А." # Имя автора для отчёта
|
||||||
|
verify_ssl: true # Проверка SSL-сертификата
|
||||||
|
|
||||||
|
period:
|
||||||
|
precision: date # date | datetime
|
||||||
|
default_from: "2026-06-01" # Начало периода по умолчанию
|
||||||
|
default_to: "2026-06-30" # Конец периода по умолчанию
|
||||||
|
dynamic: false # Автоматически сдвигать период
|
||||||
|
|
||||||
|
output:
|
||||||
|
dir: ~/reports # Директория для отчётов
|
||||||
|
filename: "{author}_{from}_{to}.{ext}" # Шаблон имени файла
|
||||||
|
default_format: xlsx # Формат по умолчанию
|
||||||
|
|
||||||
|
email:
|
||||||
|
smtp:
|
||||||
|
host: smtp.example.com
|
||||||
|
port: 587
|
||||||
|
user: bot@example.com
|
||||||
|
password: ${SMTP_PASSWORD}
|
||||||
|
tls: true
|
||||||
|
from: bot@example.com
|
||||||
|
to:
|
||||||
|
- boss@example.com
|
||||||
|
subject: "Отчёт {author} за {period}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаблон имени файла
|
||||||
|
|
||||||
|
Поле `output.filename` поддерживает подстановки:
|
||||||
|
|
||||||
|
| Плейсхолдер | Описание | Пример |
|
||||||
|
|-------------|-------------------------------|------------------------|
|
||||||
|
| `{author}` | Имя автора (пробелы → `_`) | `Кокос_А.А.` |
|
||||||
|
| `{from}` | Начало периода, `YYYY-MM-DD` | `2026-06-01` |
|
||||||
|
| `{to}` | Конец периода, `YYYY-MM-DD` | `2026-06-30` |
|
||||||
|
| `{date}` | Конец периода, `DD_MM_YYYY` | `30_06_2026` |
|
||||||
|
| `{ext}` | Расширение файла без точки | `xlsx` |
|
||||||
|
|
||||||
|
Неизвестные плейсхолдеры остаются в имени как есть.
|
||||||
|
|
||||||
|
Примеры шаблонов:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# По умолчанию
|
||||||
|
filename: "{author}_{from}_{to}.{ext}" # → Кокос_А.А._2026-06-01_2026-06-30.xlsx
|
||||||
|
|
||||||
|
# Русский формат даты
|
||||||
|
filename: "отчёт_{date}.{ext}" # → отчёт_30_06_2026.xlsx
|
||||||
|
|
||||||
|
# Без автора, только диапазон
|
||||||
|
filename: "report_{from}--{to}.{ext}" # → report_2026-06-01--2026-06-30.xlsx
|
||||||
|
```
|
||||||
|
|
||||||
|
### Подстановка переменных окружения
|
||||||
|
|
||||||
|
В любом строковом значении YAML можно использовать `${VAR}` — при загрузке
|
||||||
|
оно заменяется на значение переменной окружения:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
redmine:
|
||||||
|
api_key: ${REDMINE_API_KEY}
|
||||||
|
|
||||||
|
email:
|
||||||
|
smtp:
|
||||||
|
password: ${SMTP_PASSWORD}
|
||||||
|
```
|
||||||
|
|
||||||
|
Это безопаснее, чем хранить секреты plaintext в YAML. Однако plaintext-секреты
|
||||||
|
**не запрещены** — если вписать `api_key: "abc123"` напрямую, система примет.
|
||||||
|
Права `0600` — единственная защита.
|
||||||
|
|
||||||
|
## Миграция с `.env` на YAML
|
||||||
|
|
||||||
|
### Быстрый старт
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Генерируем YAML из текущего .env
|
||||||
|
redmine-reporter --init-config
|
||||||
|
|
||||||
|
# 2. Проверяем, что создалось
|
||||||
|
cat ~/.config/redmine-reporter/config.yml
|
||||||
|
|
||||||
|
# 3. Редактируем под себя (шаблон имени, почту, etc.)
|
||||||
|
vim ~/.config/redmine-reporter/config.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
### Что делает `--init-config`
|
||||||
|
|
||||||
|
- Читает текущие значения из `.env` и переменных окружения.
|
||||||
|
- Формирует YAML со всеми секциями (`redmine`, `period`, `output`, `email`).
|
||||||
|
- Секреты (`REDMINE_API_KEY`, `SMTP_PASSWORD`) записывает как `${VAR}`, если
|
||||||
|
переменная существует, иначе — пустая строка.
|
||||||
|
- Создаёт файл с правами `0600`, директорию — с `0700`.
|
||||||
|
|
||||||
|
### Флаги миграции
|
||||||
|
|
||||||
|
| Флаг | Назначение |
|
||||||
|
|-----------------------|-------------------------------------------------|
|
||||||
|
| `--init-config` | Создать YAML и выйти |
|
||||||
|
| `--init-config --force` | Перезаписать существующий YAML |
|
||||||
|
| `--config-path PATH` | Сохранить YAML по указанному пути (по умолчанию `~/.config/redmine-reporter/config.yml`) |
|
||||||
|
|
||||||
|
### Проверка после миграции
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Запустить без .env в текущей директории
|
||||||
|
cd /tmp
|
||||||
|
redmine-reporter --date 2026-06-01--2026-06-30
|
||||||
|
```
|
||||||
|
|
||||||
|
Если отработал — YAML-конфиг читается корректно. Если `REDMINE_URL is required` —
|
||||||
|
проверь права:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -la ~/.config/redmine-reporter/
|
||||||
|
# config.yml должно быть -rw------- (600)
|
||||||
|
# директория должна быть drwx------ (700)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Сосуществование `.env` и YAML
|
||||||
|
|
||||||
|
Можно оставить оба источника. `.env` имеет **более высокий приоритет**, чем YAML:
|
||||||
|
|
||||||
|
```
|
||||||
|
.env значения переопределяют YAML, если заданы
|
||||||
|
YAML работает как базовый слой для всего, что не в .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Это safe — если с YAML что-то пойдёт не так, просто положи `.env` обратно.
|
||||||
|
|
||||||
|
### Откат
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Удалить YAML-конфиг — система вернётся на .env
|
||||||
|
rm ~/.config/redmine-reporter/config.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
## `.env` (legacy)
|
||||||
|
|
||||||
|
Для обратной совместимости `.env` продолжает работать без изменений.
|
||||||
|
|
||||||
|
```ini
|
||||||
|
REDMINE_URL=https://red.eltex.loc
|
||||||
|
REDMINE_API_KEY=your-api-key
|
||||||
|
REDMINE_AUTHOR=Кокос А.А.
|
||||||
|
DEFAULT_FROM_DATE=2026-06-01
|
||||||
|
DEFAULT_TO_DATE=2026-06-30
|
||||||
|
```
|
||||||
|
|
||||||
|
Если ни `.env`, ни YAML не заданы — используются кодовые дефолты (текущий месяц
|
||||||
|
как период, стандартный путь сертификатов, пустой автор).
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
- YAML-конфиг: права `0600`, директория `0700`.
|
||||||
|
- Права шире `0600` → warning в stderr при каждом запуске.
|
||||||
|
- Секреты рекомендуется хранить через `${VAR}`, а не plaintext.
|
||||||
|
- `.env` **не рекомендуется** для постоянных настроек — оставьте его только
|
||||||
|
для CI/CD или временных переопределений.
|
||||||
@@ -56,3 +56,46 @@ def check_file_permissions(path: Path) -> list[str]:
|
|||||||
f"Expected 0600. Fix with: chmod 600 {path}"
|
f"Expected 0600. Fix with: chmod 600 {path}"
|
||||||
)
|
)
|
||||||
return warnings
|
return warnings
|
||||||
|
|
||||||
|
|
||||||
|
def expand_filename_template(
|
||||||
|
template: str,
|
||||||
|
*,
|
||||||
|
author: str = "",
|
||||||
|
from_date: str = "",
|
||||||
|
to_date: str = "",
|
||||||
|
ext: str = "",
|
||||||
|
) -> str:
|
||||||
|
"""Expand placeholders in a filename template.
|
||||||
|
|
||||||
|
Supported placeholders:
|
||||||
|
{author} — author name
|
||||||
|
{from} — start date (YYYY-MM-DD)
|
||||||
|
{to} — end date (YYYY-MM-DD)
|
||||||
|
{date} — end date formatted as DD_MM_YYYY
|
||||||
|
{ext} — file extension without dot
|
||||||
|
|
||||||
|
Unknown placeholders are left as-is.
|
||||||
|
"""
|
||||||
|
date_dd_mm_yyyy = ""
|
||||||
|
if to_date:
|
||||||
|
try:
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
dt = datetime.strptime(to_date, "%Y-%m-%d")
|
||||||
|
date_dd_mm_yyyy = dt.strftime("%d_%m_%Y")
|
||||||
|
except ValueError:
|
||||||
|
date_dd_mm_yyyy = to_date
|
||||||
|
|
||||||
|
replacements = {
|
||||||
|
"author": author.replace(" ", "_"),
|
||||||
|
"from": from_date,
|
||||||
|
"to": to_date,
|
||||||
|
"date": date_dd_mm_yyyy,
|
||||||
|
"ext": ext,
|
||||||
|
}
|
||||||
|
|
||||||
|
result = template
|
||||||
|
for key, value in replacements.items():
|
||||||
|
result = result.replace("{" + key + "}", value)
|
||||||
|
return result
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from unittest import mock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from redmine_reporter.config import AppConfig, Config, DEFAULT_REDMINE_VERIFY
|
from redmine_reporter.config import DEFAULT_REDMINE_VERIFY, AppConfig, Config
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.dict(
|
@mock.patch.dict(
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import tempfile
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
|
||||||
from redmine_reporter.yaml_config import (
|
from redmine_reporter.yaml_config import (
|
||||||
check_file_permissions,
|
check_file_permissions,
|
||||||
ensure_config_dir,
|
ensure_config_dir,
|
||||||
|
expand_filename_template,
|
||||||
resolve_env_vars,
|
resolve_env_vars,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -78,3 +78,47 @@ class TestCheckFilePermissions:
|
|||||||
f.chmod(0o600)
|
f.chmod(0o600)
|
||||||
warnings = check_file_permissions(f)
|
warnings = check_file_permissions(f)
|
||||||
assert warnings == []
|
assert warnings == []
|
||||||
|
|
||||||
|
|
||||||
|
class TestExpandFilenameTemplate:
|
||||||
|
"""Tests for filename template expansion."""
|
||||||
|
|
||||||
|
def test_expands_all_placeholders(self):
|
||||||
|
result = expand_filename_template(
|
||||||
|
"report_{author}_{from}_{to}.{ext}",
|
||||||
|
author="Кокос А.А.",
|
||||||
|
from_date="2026-06-01",
|
||||||
|
to_date="2026-06-30",
|
||||||
|
ext="xlsx",
|
||||||
|
)
|
||||||
|
assert result == "report_Кокос_А.А._2026-06-01_2026-06-30.xlsx"
|
||||||
|
|
||||||
|
def test_date_placeholder_dd_mm_yyyy(self):
|
||||||
|
result = expand_filename_template(
|
||||||
|
"отчёт_{date}.{ext}",
|
||||||
|
author="Кокос А.А.",
|
||||||
|
from_date="2026-03-01",
|
||||||
|
to_date="2026-03-31",
|
||||||
|
ext="odt",
|
||||||
|
)
|
||||||
|
assert result == "отчёт_31_03_2026.odt"
|
||||||
|
|
||||||
|
def test_no_placeholders_returns_unchanged(self):
|
||||||
|
result = expand_filename_template(
|
||||||
|
"report.odt",
|
||||||
|
author="Кокос А.А.",
|
||||||
|
from_date="2026-01-01",
|
||||||
|
to_date="2026-01-31",
|
||||||
|
ext="odt",
|
||||||
|
)
|
||||||
|
assert result == "report.odt"
|
||||||
|
|
||||||
|
def test_unknown_placeholder_left_as_is(self):
|
||||||
|
result = expand_filename_template(
|
||||||
|
"{author}_{unknown}.{ext}",
|
||||||
|
author="Кокос А.А.",
|
||||||
|
from_date="2026-01-01",
|
||||||
|
to_date="2026-01-31",
|
||||||
|
ext="xlsx",
|
||||||
|
)
|
||||||
|
assert result == "Кокос_А.А._{unknown}.xlsx"
|
||||||
|
|||||||
Reference in New Issue
Block a user