"""CLI: пакетная генерация документов без веб-сервера. Примеры: python -m certgen.gen --template classic --fio "Иванов Иван Иванович" \ --body "за успешное прохождение курса" python -m certgen.gen --template letter --csv names.csv --outdir out В CSV первая строка может быть заголовком (fio,title,body,date,prefix); допускаются разделители ',' и ';'. Если колонок нет — берётся первый столбец как ФИО. """ from __future__ import annotations import argparse import csv import json import re import shutil from pathlib import Path from . import config, store from .service import create_certificate def _read_csv(path: Path) -> list[dict]: text = path.read_text(encoding="utf-8-sig") first = text.splitlines()[0] if text.strip() else "" delim = ";" if first.count(";") > first.count(",") else "," rows = list(csv.reader(text.splitlines(), delimiter=delim)) rows = [r for r in rows if any(c.strip() for c in r)] if not rows: return [] header = [c.strip().lower() for c in rows[0]] known = {"fio", "фио", "title", "заголовок", "body", "текст", "date", "дата", "prefix", "префикс", "number", "номер", "email", "почта"} has_header = bool(known & set(header)) records = [] data = rows[1:] if has_header else rows keys = header if has_header else ["fio"] for row in data: rec = {} if has_header: for i, key in enumerate(keys): rec[key] = row[i] if i < len(row) else "" else: rec["fio"] = row[0] # нормализуем русские заголовки for ru, en in (("фио", "fio"), ("заголовок", "title"), ("текст", "body"), ("дата", "date"), ("префикс", "prefix"), ("номер", "number"), ("почта", "email")): if ru in rec and en not in rec: rec[en] = rec.pop(ru) records.append(rec) return records def main() -> None: p = argparse.ArgumentParser(prog="certgen.gen", description="Пакетная генерация документов") p.add_argument("--template", default="classic", help="slug шаблона") p.add_argument("--fio", help="ФИО для одного документа") p.add_argument("--email", help="email получателя документа") p.add_argument("--title", help="заголовок") p.add_argument("--body", help="дополнительный текст") p.add_argument("--date", help="дата") p.add_argument("--prefix", help="префикс номера") p.add_argument("--number", help="особый номер из резерва 1..(START-1)") p.add_argument("--csv", type=Path, help="CSV со списком") p.add_argument("--names", help="список ФИО через ';'") p.add_argument("--outdir", type=Path, help="куда скопировать готовые файлы") p.add_argument("--base", help="базовый URL для ссылок") p.add_argument("--json", action="store_true", help="вывести результат как JSON") args = p.parse_args() config.ensure_dirs() store.init_db() template = store.get_template(args.template) if not template: raise SystemExit(f"Неизвестный шаблон: {args.template}. " f"Доступны: {[t['slug'] for t in store.list_templates()]}") records: list[dict] = [] if args.csv: records = _read_csv(args.csv) elif args.names: records = [{"fio": n.strip()} for n in args.names.split(";") if n.strip()] elif args.fio: records = [{"fio": args.fio}] else: raise SystemExit("Укажите --fio, --names или --csv") results = [] for rec in records: body = {"fio": rec.get("fio", "")} for key, val in (("title", args.title), ("body", args.body), ("date", args.date), ("prefix", args.prefix), ("number", args.number), ("email", args.email)): value = rec.get(key) or val if value: body[key] = value cert = create_certificate(template, body, base=args.base) if args.outdir: args.outdir.mkdir(parents=True, exist_ok=True) safe = re.sub(r"[^\w.-]+", "_", cert["number"]) or str(cert["seq"]) shutil.copyfile(cert["png_path"], args.outdir / f"{safe}.png") shutil.copyfile(cert["pdf_path"], args.outdir / f"{safe}.pdf") results.append(cert) if not args.json: print(f"№ {cert['number']} {cert['fio']} -> {cert['url']}") if args.json: print(json.dumps(results, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()