From b1a565bc9e6b7b6022d245f076d653b1c110ee8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D0=BA=D0=BE=D1=81=20=D0=90=D1=80=D1=82=D0=B5?= =?UTF-8?q?=D0=BC=20=D0=9D=D0=B8=D0=BA=D0=BE=D0=BB=D0=B0=D0=B5=D0=B2=D0=B8?= =?UTF-8?q?=D1=87?= Date: Fri, 3 Jul 2026 18:13:55 +0700 Subject: [PATCH] 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 --- docs/CONFIG.md | 188 ++++++++++++++++++++++++++++++++ redmine_reporter/yaml_config.py | 43 ++++++++ tests/test_config.py | 2 +- tests/test_yaml_config.py | 46 +++++++- 4 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 docs/CONFIG.md diff --git a/docs/CONFIG.md b/docs/CONFIG.md new file mode 100644 index 0000000..515843a --- /dev/null +++ b/docs/CONFIG.md @@ -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 или временных переопределений. diff --git a/redmine_reporter/yaml_config.py b/redmine_reporter/yaml_config.py index 42d35d8..153dfa0 100644 --- a/redmine_reporter/yaml_config.py +++ b/redmine_reporter/yaml_config.py @@ -56,3 +56,46 @@ def check_file_permissions(path: Path) -> list[str]: f"Expected 0600. Fix with: chmod 600 {path}" ) 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 diff --git a/tests/test_config.py b/tests/test_config.py index 7eea661..3b6b9bf 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,7 +5,7 @@ from unittest import mock 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( diff --git a/tests/test_yaml_config.py b/tests/test_yaml_config.py index 8c4f619..c90b50c 100644 --- a/tests/test_yaml_config.py +++ b/tests/test_yaml_config.py @@ -4,10 +4,10 @@ import tempfile from pathlib import Path from unittest import mock - from redmine_reporter.yaml_config import ( check_file_permissions, ensure_config_dir, + expand_filename_template, resolve_env_vars, ) @@ -78,3 +78,47 @@ class TestCheckFilePermissions: f.chmod(0o600) warnings = check_file_permissions(f) 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"