Skip to content

Instantly share code, notes, and snippets.

@frederik-elwert
Last active August 22, 2025 15:54
Show Gist options
  • Save frederik-elwert/5d0fee06aad946c75ef03084a2684233 to your computer and use it in GitHub Desktop.
Save frederik-elwert/5d0fee06aad946c75ef03084a2684233 to your computer and use it in GitHub Desktop.
Script that performs mail merge using pandoc, jinja2, and pypdf2.
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "jinja2",
# "pypdf2",
# "tqdm",
# ]
# ///
import argparse
import csv
import logging
import subprocess
import sys
import tempfile
from pathlib import Path
from jinja2 import Template
from PyPDF2 import PdfMerger
from tqdm import tqdm
def merge(input_file, csv_file, output_file, extra_args):
template = Template(input_file.read_text())
tempdir = tempfile.TemporaryDirectory(prefix="pandoc_merge_", dir=input_file.parent)
tempdir_path = Path(tempdir.name)
logging.debug(f"Using temporary directory {tempdir_path}.")
output_files = []
with csv_file.open() as csvfile:
# Make list to get proper progress bar
rows = list(enumerate(csv.DictReader(csvfile)))
for i, data in tqdm(rows):
content = template.render(**data)
tmpname = f"temp{i:05d}"
tmppath = (tempdir_path / tmpname).with_suffix(output_file.suffix)
output_files.append(tmppath)
cmd = ["pandoc", "-o", tmppath] + extra_args
logging.debug(f"Process file {tmppath.name}")
subprocess.run(cmd, input=content, text=True, check=True)
merger = PdfMerger()
for pdf in output_files:
merger.append(pdf)
with output_file.open("wb") as outfile:
merger.write(outfile)
tempdir.cleanup()
def main():
# Parse commandline arguments
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument("-v", "--verbose", action="store_true")
arg_parser.add_argument("-c", "--csv", type=Path)
arg_parser.add_argument("-o", "--outfile", type=Path)
arg_parser.add_argument("input", type=Path)
args, extra_args = arg_parser.parse_known_args()
# Set up logging
if args.verbose:
level = logging.DEBUG
else:
level = logging.ERROR
logging.basicConfig(level=level)
# Return exit value
merge(args.input, args.csv, args.outfile, extra_args)
return 0
if __name__ == "__main__":
sys.exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment